HubConnection.js 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. import { HandshakeProtocol } from "./HandshakeProtocol";
  4. import { AbortError } from "./Errors";
  5. import { MessageType } from "./IHubProtocol";
  6. import { LogLevel } from "./ILogger";
  7. import { Subject } from "./Subject";
  8. import { Arg, getErrorString, Platform } from "./Utils";
  9. const DEFAULT_TIMEOUT_IN_MS = 30 * 1000;
  10. const DEFAULT_PING_INTERVAL_IN_MS = 15 * 1000;
  11. /** Describes the current state of the {@link HubConnection} to the server. */
  12. export var HubConnectionState;
  13. (function (HubConnectionState) {
  14. /** The hub connection is disconnected. */
  15. HubConnectionState["Disconnected"] = "Disconnected";
  16. /** The hub connection is connecting. */
  17. HubConnectionState["Connecting"] = "Connecting";
  18. /** The hub connection is connected. */
  19. HubConnectionState["Connected"] = "Connected";
  20. /** The hub connection is disconnecting. */
  21. HubConnectionState["Disconnecting"] = "Disconnecting";
  22. /** The hub connection is reconnecting. */
  23. HubConnectionState["Reconnecting"] = "Reconnecting";
  24. })(HubConnectionState || (HubConnectionState = {}));
  25. /** Represents a connection to a SignalR Hub. */
  26. export class HubConnection {
  27. constructor(connection, logger, protocol, reconnectPolicy) {
  28. this._nextKeepAlive = 0;
  29. this._freezeEventListener = () => {
  30. this._logger.log(LogLevel.Warning, "The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://docs.microsoft.com/aspnet/core/signalr/javascript-client#bsleep");
  31. };
  32. Arg.isRequired(connection, "connection");
  33. Arg.isRequired(logger, "logger");
  34. Arg.isRequired(protocol, "protocol");
  35. this.serverTimeoutInMilliseconds = DEFAULT_TIMEOUT_IN_MS;
  36. this.keepAliveIntervalInMilliseconds = DEFAULT_PING_INTERVAL_IN_MS;
  37. this._logger = logger;
  38. this._protocol = protocol;
  39. this.connection = connection;
  40. this._reconnectPolicy = reconnectPolicy;
  41. this._handshakeProtocol = new HandshakeProtocol();
  42. this.connection.onreceive = (data) => this._processIncomingData(data);
  43. this.connection.onclose = (error) => this._connectionClosed(error);
  44. this._callbacks = {};
  45. this._methods = {};
  46. this._closedCallbacks = [];
  47. this._reconnectingCallbacks = [];
  48. this._reconnectedCallbacks = [];
  49. this._invocationId = 0;
  50. this._receivedHandshakeResponse = false;
  51. this._connectionState = HubConnectionState.Disconnected;
  52. this._connectionStarted = false;
  53. this._cachedPingMessage = this._protocol.writeMessage({ type: MessageType.Ping });
  54. }
  55. /** @internal */
  56. // Using a public static factory method means we can have a private constructor and an _internal_
  57. // create method that can be used by HubConnectionBuilder. An "internal" constructor would just
  58. // be stripped away and the '.d.ts' file would have no constructor, which is interpreted as a
  59. // public parameter-less constructor.
  60. static create(connection, logger, protocol, reconnectPolicy) {
  61. return new HubConnection(connection, logger, protocol, reconnectPolicy);
  62. }
  63. /** Indicates the state of the {@link HubConnection} to the server. */
  64. get state() {
  65. return this._connectionState;
  66. }
  67. /** Represents the connection id of the {@link HubConnection} on the server. The connection id will be null when the connection is either
  68. * in the disconnected state or if the negotiation step was skipped.
  69. */
  70. get connectionId() {
  71. return this.connection ? (this.connection.connectionId || null) : null;
  72. }
  73. /** Indicates the url of the {@link HubConnection} to the server. */
  74. get baseUrl() {
  75. return this.connection.baseUrl || "";
  76. }
  77. /**
  78. * Sets a new url for the HubConnection. Note that the url can only be changed when the connection is in either the Disconnected or
  79. * Reconnecting states.
  80. * @param {string} url The url to connect to.
  81. */
  82. set baseUrl(url) {
  83. if (this._connectionState !== HubConnectionState.Disconnected && this._connectionState !== HubConnectionState.Reconnecting) {
  84. throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");
  85. }
  86. if (!url) {
  87. throw new Error("The HubConnection url must be a valid url.");
  88. }
  89. this.connection.baseUrl = url;
  90. }
  91. /** Starts the connection.
  92. *
  93. * @returns {Promise<void>} A Promise that resolves when the connection has been successfully established, or rejects with an error.
  94. */
  95. start() {
  96. this._startPromise = this._startWithStateTransitions();
  97. return this._startPromise;
  98. }
  99. async _startWithStateTransitions() {
  100. if (this._connectionState !== HubConnectionState.Disconnected) {
  101. return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));
  102. }
  103. this._connectionState = HubConnectionState.Connecting;
  104. this._logger.log(LogLevel.Debug, "Starting HubConnection.");
  105. try {
  106. await this._startInternal();
  107. if (Platform.isBrowser) {
  108. // Log when the browser freezes the tab so users know why their connection unexpectedly stopped working
  109. window.document.addEventListener("freeze", this._freezeEventListener);
  110. }
  111. this._connectionState = HubConnectionState.Connected;
  112. this._connectionStarted = true;
  113. this._logger.log(LogLevel.Debug, "HubConnection connected successfully.");
  114. }
  115. catch (e) {
  116. this._connectionState = HubConnectionState.Disconnected;
  117. this._logger.log(LogLevel.Debug, `HubConnection failed to start successfully because of error '${e}'.`);
  118. return Promise.reject(e);
  119. }
  120. }
  121. async _startInternal() {
  122. this._stopDuringStartError = undefined;
  123. this._receivedHandshakeResponse = false;
  124. // Set up the promise before any connection is (re)started otherwise it could race with received messages
  125. const handshakePromise = new Promise((resolve, reject) => {
  126. this._handshakeResolver = resolve;
  127. this._handshakeRejecter = reject;
  128. });
  129. await this.connection.start(this._protocol.transferFormat);
  130. try {
  131. const handshakeRequest = {
  132. protocol: this._protocol.name,
  133. version: this._protocol.version,
  134. };
  135. this._logger.log(LogLevel.Debug, "Sending handshake request.");
  136. await this._sendMessage(this._handshakeProtocol.writeHandshakeRequest(handshakeRequest));
  137. this._logger.log(LogLevel.Information, `Using HubProtocol '${this._protocol.name}'.`);
  138. // defensively cleanup timeout in case we receive a message from the server before we finish start
  139. this._cleanupTimeout();
  140. this._resetTimeoutPeriod();
  141. this._resetKeepAliveInterval();
  142. await handshakePromise;
  143. // It's important to check the stopDuringStartError instead of just relying on the handshakePromise
  144. // being rejected on close, because this continuation can run after both the handshake completed successfully
  145. // and the connection was closed.
  146. if (this._stopDuringStartError) {
  147. // It's important to throw instead of returning a rejected promise, because we don't want to allow any state
  148. // transitions to occur between now and the calling code observing the exceptions. Returning a rejected promise
  149. // will cause the calling continuation to get scheduled to run later.
  150. // eslint-disable-next-line @typescript-eslint/no-throw-literal
  151. throw this._stopDuringStartError;
  152. }
  153. if (!this.connection.features.inherentKeepAlive) {
  154. await this._sendMessage(this._cachedPingMessage);
  155. }
  156. }
  157. catch (e) {
  158. this._logger.log(LogLevel.Debug, `Hub handshake failed with error '${e}' during start(). Stopping HubConnection.`);
  159. this._cleanupTimeout();
  160. this._cleanupPingTimer();
  161. // HttpConnection.stop() should not complete until after the onclose callback is invoked.
  162. // This will transition the HubConnection to the disconnected state before HttpConnection.stop() completes.
  163. await this.connection.stop(e);
  164. throw e;
  165. }
  166. }
  167. /** Stops the connection.
  168. *
  169. * @returns {Promise<void>} A Promise that resolves when the connection has been successfully terminated, or rejects with an error.
  170. */
  171. async stop() {
  172. // Capture the start promise before the connection might be restarted in an onclose callback.
  173. const startPromise = this._startPromise;
  174. this._stopPromise = this._stopInternal();
  175. await this._stopPromise;
  176. try {
  177. // Awaiting undefined continues immediately
  178. await startPromise;
  179. }
  180. catch (e) {
  181. // This exception is returned to the user as a rejected Promise from the start method.
  182. }
  183. }
  184. _stopInternal(error) {
  185. if (this._connectionState === HubConnectionState.Disconnected) {
  186. this._logger.log(LogLevel.Debug, `Call to HubConnection.stop(${error}) ignored because it is already in the disconnected state.`);
  187. return Promise.resolve();
  188. }
  189. if (this._connectionState === HubConnectionState.Disconnecting) {
  190. this._logger.log(LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnecting state.`);
  191. return this._stopPromise;
  192. }
  193. this._connectionState = HubConnectionState.Disconnecting;
  194. this._logger.log(LogLevel.Debug, "Stopping HubConnection.");
  195. if (this._reconnectDelayHandle) {
  196. // We're in a reconnect delay which means the underlying connection is currently already stopped.
  197. // Just clear the handle to stop the reconnect loop (which no one is waiting on thankfully) and
  198. // fire the onclose callbacks.
  199. this._logger.log(LogLevel.Debug, "Connection stopped during reconnect delay. Done reconnecting.");
  200. clearTimeout(this._reconnectDelayHandle);
  201. this._reconnectDelayHandle = undefined;
  202. this._completeClose();
  203. return Promise.resolve();
  204. }
  205. this._cleanupTimeout();
  206. this._cleanupPingTimer();
  207. this._stopDuringStartError = error || new AbortError("The connection was stopped before the hub handshake could complete.");
  208. // HttpConnection.stop() should not complete until after either HttpConnection.start() fails
  209. // or the onclose callback is invoked. The onclose callback will transition the HubConnection
  210. // to the disconnected state if need be before HttpConnection.stop() completes.
  211. return this.connection.stop(error);
  212. }
  213. /** Invokes a streaming hub method on the server using the specified name and arguments.
  214. *
  215. * @typeparam T The type of the items returned by the server.
  216. * @param {string} methodName The name of the server method to invoke.
  217. * @param {any[]} args The arguments used to invoke the server method.
  218. * @returns {IStreamResult<T>} An object that yields results from the server as they are received.
  219. */
  220. stream(methodName, ...args) {
  221. const [streams, streamIds] = this._replaceStreamingParams(args);
  222. const invocationDescriptor = this._createStreamInvocation(methodName, args, streamIds);
  223. // eslint-disable-next-line prefer-const
  224. let promiseQueue;
  225. const subject = new Subject();
  226. subject.cancelCallback = () => {
  227. const cancelInvocation = this._createCancelInvocation(invocationDescriptor.invocationId);
  228. delete this._callbacks[invocationDescriptor.invocationId];
  229. return promiseQueue.then(() => {
  230. return this._sendWithProtocol(cancelInvocation);
  231. });
  232. };
  233. this._callbacks[invocationDescriptor.invocationId] = (invocationEvent, error) => {
  234. if (error) {
  235. subject.error(error);
  236. return;
  237. }
  238. else if (invocationEvent) {
  239. // invocationEvent will not be null when an error is not passed to the callback
  240. if (invocationEvent.type === MessageType.Completion) {
  241. if (invocationEvent.error) {
  242. subject.error(new Error(invocationEvent.error));
  243. }
  244. else {
  245. subject.complete();
  246. }
  247. }
  248. else {
  249. subject.next((invocationEvent.item));
  250. }
  251. }
  252. };
  253. promiseQueue = this._sendWithProtocol(invocationDescriptor)
  254. .catch((e) => {
  255. subject.error(e);
  256. delete this._callbacks[invocationDescriptor.invocationId];
  257. });
  258. this._launchStreams(streams, promiseQueue);
  259. return subject;
  260. }
  261. _sendMessage(message) {
  262. this._resetKeepAliveInterval();
  263. return this.connection.send(message);
  264. }
  265. /**
  266. * Sends a js object to the server.
  267. * @param message The js object to serialize and send.
  268. */
  269. _sendWithProtocol(message) {
  270. return this._sendMessage(this._protocol.writeMessage(message));
  271. }
  272. /** Invokes a hub method on the server using the specified name and arguments. Does not wait for a response from the receiver.
  273. *
  274. * The Promise returned by this method resolves when the client has sent the invocation to the server. The server may still
  275. * be processing the invocation.
  276. *
  277. * @param {string} methodName The name of the server method to invoke.
  278. * @param {any[]} args The arguments used to invoke the server method.
  279. * @returns {Promise<void>} A Promise that resolves when the invocation has been successfully sent, or rejects with an error.
  280. */
  281. send(methodName, ...args) {
  282. const [streams, streamIds] = this._replaceStreamingParams(args);
  283. const sendPromise = this._sendWithProtocol(this._createInvocation(methodName, args, true, streamIds));
  284. this._launchStreams(streams, sendPromise);
  285. return sendPromise;
  286. }
  287. /** Invokes a hub method on the server using the specified name and arguments.
  288. *
  289. * The Promise returned by this method resolves when the server indicates it has finished invoking the method. When the promise
  290. * resolves, the server has finished invoking the method. If the server method returns a result, it is produced as the result of
  291. * resolving the Promise.
  292. *
  293. * @typeparam T The expected return type.
  294. * @param {string} methodName The name of the server method to invoke.
  295. * @param {any[]} args The arguments used to invoke the server method.
  296. * @returns {Promise<T>} A Promise that resolves with the result of the server method (if any), or rejects with an error.
  297. */
  298. invoke(methodName, ...args) {
  299. const [streams, streamIds] = this._replaceStreamingParams(args);
  300. const invocationDescriptor = this._createInvocation(methodName, args, false, streamIds);
  301. const p = new Promise((resolve, reject) => {
  302. // invocationId will always have a value for a non-blocking invocation
  303. this._callbacks[invocationDescriptor.invocationId] = (invocationEvent, error) => {
  304. if (error) {
  305. reject(error);
  306. return;
  307. }
  308. else if (invocationEvent) {
  309. // invocationEvent will not be null when an error is not passed to the callback
  310. if (invocationEvent.type === MessageType.Completion) {
  311. if (invocationEvent.error) {
  312. reject(new Error(invocationEvent.error));
  313. }
  314. else {
  315. resolve(invocationEvent.result);
  316. }
  317. }
  318. else {
  319. reject(new Error(`Unexpected message type: ${invocationEvent.type}`));
  320. }
  321. }
  322. };
  323. const promiseQueue = this._sendWithProtocol(invocationDescriptor)
  324. .catch((e) => {
  325. reject(e);
  326. // invocationId will always have a value for a non-blocking invocation
  327. delete this._callbacks[invocationDescriptor.invocationId];
  328. });
  329. this._launchStreams(streams, promiseQueue);
  330. });
  331. return p;
  332. }
  333. on(methodName, newMethod) {
  334. if (!methodName || !newMethod) {
  335. return;
  336. }
  337. methodName = methodName.toLowerCase();
  338. if (!this._methods[methodName]) {
  339. this._methods[methodName] = [];
  340. }
  341. // Preventing adding the same handler multiple times.
  342. if (this._methods[methodName].indexOf(newMethod) !== -1) {
  343. return;
  344. }
  345. this._methods[methodName].push(newMethod);
  346. }
  347. off(methodName, method) {
  348. if (!methodName) {
  349. return;
  350. }
  351. methodName = methodName.toLowerCase();
  352. const handlers = this._methods[methodName];
  353. if (!handlers) {
  354. return;
  355. }
  356. if (method) {
  357. const removeIdx = handlers.indexOf(method);
  358. if (removeIdx !== -1) {
  359. handlers.splice(removeIdx, 1);
  360. if (handlers.length === 0) {
  361. delete this._methods[methodName];
  362. }
  363. }
  364. }
  365. else {
  366. delete this._methods[methodName];
  367. }
  368. }
  369. /** Registers a handler that will be invoked when the connection is closed.
  370. *
  371. * @param {Function} callback The handler that will be invoked when the connection is closed. Optionally receives a single argument containing the error that caused the connection to close (if any).
  372. */
  373. onclose(callback) {
  374. if (callback) {
  375. this._closedCallbacks.push(callback);
  376. }
  377. }
  378. /** Registers a handler that will be invoked when the connection starts reconnecting.
  379. *
  380. * @param {Function} callback The handler that will be invoked when the connection starts reconnecting. Optionally receives a single argument containing the error that caused the connection to start reconnecting (if any).
  381. */
  382. onreconnecting(callback) {
  383. if (callback) {
  384. this._reconnectingCallbacks.push(callback);
  385. }
  386. }
  387. /** Registers a handler that will be invoked when the connection successfully reconnects.
  388. *
  389. * @param {Function} callback The handler that will be invoked when the connection successfully reconnects.
  390. */
  391. onreconnected(callback) {
  392. if (callback) {
  393. this._reconnectedCallbacks.push(callback);
  394. }
  395. }
  396. _processIncomingData(data) {
  397. this._cleanupTimeout();
  398. if (!this._receivedHandshakeResponse) {
  399. data = this._processHandshakeResponse(data);
  400. this._receivedHandshakeResponse = true;
  401. }
  402. // Data may have all been read when processing handshake response
  403. if (data) {
  404. // Parse the messages
  405. const messages = this._protocol.parseMessages(data, this._logger);
  406. for (const message of messages) {
  407. switch (message.type) {
  408. case MessageType.Invocation:
  409. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  410. this._invokeClientMethod(message);
  411. break;
  412. case MessageType.StreamItem:
  413. case MessageType.Completion: {
  414. const callback = this._callbacks[message.invocationId];
  415. if (callback) {
  416. if (message.type === MessageType.Completion) {
  417. delete this._callbacks[message.invocationId];
  418. }
  419. try {
  420. callback(message);
  421. }
  422. catch (e) {
  423. this._logger.log(LogLevel.Error, `Stream callback threw error: ${getErrorString(e)}`);
  424. }
  425. }
  426. break;
  427. }
  428. case MessageType.Ping:
  429. // Don't care about pings
  430. break;
  431. case MessageType.Close: {
  432. this._logger.log(LogLevel.Information, "Close message received from server.");
  433. const error = message.error ? new Error("Server returned an error on close: " + message.error) : undefined;
  434. if (message.allowReconnect === true) {
  435. // It feels wrong not to await connection.stop() here, but processIncomingData is called as part of an onreceive callback which is not async,
  436. // this is already the behavior for serverTimeout(), and HttpConnection.Stop() should catch and log all possible exceptions.
  437. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  438. this.connection.stop(error);
  439. }
  440. else {
  441. // We cannot await stopInternal() here, but subsequent calls to stop() will await this if stopInternal() is still ongoing.
  442. this._stopPromise = this._stopInternal(error);
  443. }
  444. break;
  445. }
  446. default:
  447. this._logger.log(LogLevel.Warning, `Invalid message type: ${message.type}.`);
  448. break;
  449. }
  450. }
  451. }
  452. this._resetTimeoutPeriod();
  453. }
  454. _processHandshakeResponse(data) {
  455. let responseMessage;
  456. let remainingData;
  457. try {
  458. [remainingData, responseMessage] = this._handshakeProtocol.parseHandshakeResponse(data);
  459. }
  460. catch (e) {
  461. const message = "Error parsing handshake response: " + e;
  462. this._logger.log(LogLevel.Error, message);
  463. const error = new Error(message);
  464. this._handshakeRejecter(error);
  465. throw error;
  466. }
  467. if (responseMessage.error) {
  468. const message = "Server returned handshake error: " + responseMessage.error;
  469. this._logger.log(LogLevel.Error, message);
  470. const error = new Error(message);
  471. this._handshakeRejecter(error);
  472. throw error;
  473. }
  474. else {
  475. this._logger.log(LogLevel.Debug, "Server handshake complete.");
  476. }
  477. this._handshakeResolver();
  478. return remainingData;
  479. }
  480. _resetKeepAliveInterval() {
  481. if (this.connection.features.inherentKeepAlive) {
  482. return;
  483. }
  484. // Set the time we want the next keep alive to be sent
  485. // Timer will be setup on next message receive
  486. this._nextKeepAlive = new Date().getTime() + this.keepAliveIntervalInMilliseconds;
  487. this._cleanupPingTimer();
  488. }
  489. _resetTimeoutPeriod() {
  490. if (!this.connection.features || !this.connection.features.inherentKeepAlive) {
  491. // Set the timeout timer
  492. this._timeoutHandle = setTimeout(() => this.serverTimeout(), this.serverTimeoutInMilliseconds);
  493. // Set keepAlive timer if there isn't one
  494. if (this._pingServerHandle === undefined) {
  495. let nextPing = this._nextKeepAlive - new Date().getTime();
  496. if (nextPing < 0) {
  497. nextPing = 0;
  498. }
  499. // The timer needs to be set from a networking callback to avoid Chrome timer throttling from causing timers to run once a minute
  500. this._pingServerHandle = setTimeout(async () => {
  501. if (this._connectionState === HubConnectionState.Connected) {
  502. try {
  503. await this._sendMessage(this._cachedPingMessage);
  504. }
  505. catch {
  506. // We don't care about the error. It should be seen elsewhere in the client.
  507. // The connection is probably in a bad or closed state now, cleanup the timer so it stops triggering
  508. this._cleanupPingTimer();
  509. }
  510. }
  511. }, nextPing);
  512. }
  513. }
  514. }
  515. // eslint-disable-next-line @typescript-eslint/naming-convention
  516. serverTimeout() {
  517. // The server hasn't talked to us in a while. It doesn't like us anymore ... :(
  518. // Terminate the connection, but we don't need to wait on the promise. This could trigger reconnecting.
  519. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  520. this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."));
  521. }
  522. async _invokeClientMethod(invocationMessage) {
  523. const methodName = invocationMessage.target.toLowerCase();
  524. const methods = this._methods[methodName];
  525. if (!methods) {
  526. this._logger.log(LogLevel.Warning, `No client method with the name '${methodName}' found.`);
  527. // No handlers provided by client but the server is expecting a response still, so we send an error
  528. if (invocationMessage.invocationId) {
  529. this._logger.log(LogLevel.Warning, `No result given for '${methodName}' method and invocation ID '${invocationMessage.invocationId}'.`);
  530. await this._sendWithProtocol(this._createCompletionMessage(invocationMessage.invocationId, "Client didn't provide a result.", null));
  531. }
  532. return;
  533. }
  534. // Avoid issues with handlers removing themselves thus modifying the list while iterating through it
  535. const methodsCopy = methods.slice();
  536. // Server expects a response
  537. const expectsResponse = invocationMessage.invocationId ? true : false;
  538. // We preserve the last result or exception but still call all handlers
  539. let res;
  540. let exception;
  541. let completionMessage;
  542. for (const m of methodsCopy) {
  543. try {
  544. const prevRes = res;
  545. res = await m.apply(this, invocationMessage.arguments);
  546. if (expectsResponse && res && prevRes) {
  547. this._logger.log(LogLevel.Error, `Multiple results provided for '${methodName}'. Sending error to server.`);
  548. completionMessage = this._createCompletionMessage(invocationMessage.invocationId, `Client provided multiple results.`, null);
  549. }
  550. // Ignore exception if we got a result after, the exception will be logged
  551. exception = undefined;
  552. }
  553. catch (e) {
  554. exception = e;
  555. this._logger.log(LogLevel.Error, `A callback for the method '${methodName}' threw error '${e}'.`);
  556. }
  557. }
  558. if (completionMessage) {
  559. await this._sendWithProtocol(completionMessage);
  560. }
  561. else if (expectsResponse) {
  562. // If there is an exception that means either no result was given or a handler after a result threw
  563. if (exception) {
  564. completionMessage = this._createCompletionMessage(invocationMessage.invocationId, `${exception}`, null);
  565. }
  566. else if (res !== undefined) {
  567. completionMessage = this._createCompletionMessage(invocationMessage.invocationId, null, res);
  568. }
  569. else {
  570. this._logger.log(LogLevel.Warning, `No result given for '${methodName}' method and invocation ID '${invocationMessage.invocationId}'.`);
  571. // Client didn't provide a result or throw from a handler, server expects a response so we send an error
  572. completionMessage = this._createCompletionMessage(invocationMessage.invocationId, "Client didn't provide a result.", null);
  573. }
  574. await this._sendWithProtocol(completionMessage);
  575. }
  576. else {
  577. if (res) {
  578. this._logger.log(LogLevel.Error, `Result given for '${methodName}' method but server is not expecting a result.`);
  579. }
  580. }
  581. }
  582. _connectionClosed(error) {
  583. this._logger.log(LogLevel.Debug, `HubConnection.connectionClosed(${error}) called while in state ${this._connectionState}.`);
  584. // Triggering this.handshakeRejecter is insufficient because it could already be resolved without the continuation having run yet.
  585. this._stopDuringStartError = this._stopDuringStartError || error || new AbortError("The underlying connection was closed before the hub handshake could complete.");
  586. // If the handshake is in progress, start will be waiting for the handshake promise, so we complete it.
  587. // If it has already completed, this should just noop.
  588. if (this._handshakeResolver) {
  589. this._handshakeResolver();
  590. }
  591. this._cancelCallbacksWithError(error || new Error("Invocation canceled due to the underlying connection being closed."));
  592. this._cleanupTimeout();
  593. this._cleanupPingTimer();
  594. if (this._connectionState === HubConnectionState.Disconnecting) {
  595. this._completeClose(error);
  596. }
  597. else if (this._connectionState === HubConnectionState.Connected && this._reconnectPolicy) {
  598. // eslint-disable-next-line @typescript-eslint/no-floating-promises
  599. this._reconnect(error);
  600. }
  601. else if (this._connectionState === HubConnectionState.Connected) {
  602. this._completeClose(error);
  603. }
  604. // If none of the above if conditions were true were called the HubConnection must be in either:
  605. // 1. The Connecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail it.
  606. // 2. The Reconnecting state in which case the handshakeResolver will complete it and stopDuringStartError will fail the current reconnect attempt
  607. // and potentially continue the reconnect() loop.
  608. // 3. The Disconnected state in which case we're already done.
  609. }
  610. _completeClose(error) {
  611. if (this._connectionStarted) {
  612. this._connectionState = HubConnectionState.Disconnected;
  613. this._connectionStarted = false;
  614. if (Platform.isBrowser) {
  615. window.document.removeEventListener("freeze", this._freezeEventListener);
  616. }
  617. try {
  618. this._closedCallbacks.forEach((c) => c.apply(this, [error]));
  619. }
  620. catch (e) {
  621. this._logger.log(LogLevel.Error, `An onclose callback called with error '${error}' threw error '${e}'.`);
  622. }
  623. }
  624. }
  625. async _reconnect(error) {
  626. const reconnectStartTime = Date.now();
  627. let previousReconnectAttempts = 0;
  628. let retryError = error !== undefined ? error : new Error("Attempting to reconnect due to a unknown error.");
  629. let nextRetryDelay = this._getNextRetryDelay(previousReconnectAttempts++, 0, retryError);
  630. if (nextRetryDelay === null) {
  631. this._logger.log(LogLevel.Debug, "Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt.");
  632. this._completeClose(error);
  633. return;
  634. }
  635. this._connectionState = HubConnectionState.Reconnecting;
  636. if (error) {
  637. this._logger.log(LogLevel.Information, `Connection reconnecting because of error '${error}'.`);
  638. }
  639. else {
  640. this._logger.log(LogLevel.Information, "Connection reconnecting.");
  641. }
  642. if (this._reconnectingCallbacks.length !== 0) {
  643. try {
  644. this._reconnectingCallbacks.forEach((c) => c.apply(this, [error]));
  645. }
  646. catch (e) {
  647. this._logger.log(LogLevel.Error, `An onreconnecting callback called with error '${error}' threw error '${e}'.`);
  648. }
  649. // Exit early if an onreconnecting callback called connection.stop().
  650. if (this._connectionState !== HubConnectionState.Reconnecting) {
  651. this._logger.log(LogLevel.Debug, "Connection left the reconnecting state in onreconnecting callback. Done reconnecting.");
  652. return;
  653. }
  654. }
  655. while (nextRetryDelay !== null) {
  656. this._logger.log(LogLevel.Information, `Reconnect attempt number ${previousReconnectAttempts} will start in ${nextRetryDelay} ms.`);
  657. await new Promise((resolve) => {
  658. this._reconnectDelayHandle = setTimeout(resolve, nextRetryDelay);
  659. });
  660. this._reconnectDelayHandle = undefined;
  661. if (this._connectionState !== HubConnectionState.Reconnecting) {
  662. this._logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect delay. Done reconnecting.");
  663. return;
  664. }
  665. try {
  666. await this._startInternal();
  667. this._connectionState = HubConnectionState.Connected;
  668. this._logger.log(LogLevel.Information, "HubConnection reconnected successfully.");
  669. if (this._reconnectedCallbacks.length !== 0) {
  670. try {
  671. this._reconnectedCallbacks.forEach((c) => c.apply(this, [this.connection.connectionId]));
  672. }
  673. catch (e) {
  674. this._logger.log(LogLevel.Error, `An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${e}'.`);
  675. }
  676. }
  677. return;
  678. }
  679. catch (e) {
  680. this._logger.log(LogLevel.Information, `Reconnect attempt failed because of error '${e}'.`);
  681. if (this._connectionState !== HubConnectionState.Reconnecting) {
  682. this._logger.log(LogLevel.Debug, `Connection moved to the '${this._connectionState}' from the reconnecting state during reconnect attempt. Done reconnecting.`);
  683. // The TypeScript compiler thinks that connectionState must be Connected here. The TypeScript compiler is wrong.
  684. if (this._connectionState === HubConnectionState.Disconnecting) {
  685. this._completeClose();
  686. }
  687. return;
  688. }
  689. retryError = e instanceof Error ? e : new Error(e.toString());
  690. nextRetryDelay = this._getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime, retryError);
  691. }
  692. }
  693. this._logger.log(LogLevel.Information, `Reconnect retries have been exhausted after ${Date.now() - reconnectStartTime} ms and ${previousReconnectAttempts} failed attempts. Connection disconnecting.`);
  694. this._completeClose();
  695. }
  696. _getNextRetryDelay(previousRetryCount, elapsedMilliseconds, retryReason) {
  697. try {
  698. return this._reconnectPolicy.nextRetryDelayInMilliseconds({
  699. elapsedMilliseconds,
  700. previousRetryCount,
  701. retryReason,
  702. });
  703. }
  704. catch (e) {
  705. this._logger.log(LogLevel.Error, `IRetryPolicy.nextRetryDelayInMilliseconds(${previousRetryCount}, ${elapsedMilliseconds}) threw error '${e}'.`);
  706. return null;
  707. }
  708. }
  709. _cancelCallbacksWithError(error) {
  710. const callbacks = this._callbacks;
  711. this._callbacks = {};
  712. Object.keys(callbacks)
  713. .forEach((key) => {
  714. const callback = callbacks[key];
  715. try {
  716. callback(null, error);
  717. }
  718. catch (e) {
  719. this._logger.log(LogLevel.Error, `Stream 'error' callback called with '${error}' threw error: ${getErrorString(e)}`);
  720. }
  721. });
  722. }
  723. _cleanupPingTimer() {
  724. if (this._pingServerHandle) {
  725. clearTimeout(this._pingServerHandle);
  726. this._pingServerHandle = undefined;
  727. }
  728. }
  729. _cleanupTimeout() {
  730. if (this._timeoutHandle) {
  731. clearTimeout(this._timeoutHandle);
  732. }
  733. }
  734. _createInvocation(methodName, args, nonblocking, streamIds) {
  735. if (nonblocking) {
  736. if (streamIds.length !== 0) {
  737. return {
  738. arguments: args,
  739. streamIds,
  740. target: methodName,
  741. type: MessageType.Invocation,
  742. };
  743. }
  744. else {
  745. return {
  746. arguments: args,
  747. target: methodName,
  748. type: MessageType.Invocation,
  749. };
  750. }
  751. }
  752. else {
  753. const invocationId = this._invocationId;
  754. this._invocationId++;
  755. if (streamIds.length !== 0) {
  756. return {
  757. arguments: args,
  758. invocationId: invocationId.toString(),
  759. streamIds,
  760. target: methodName,
  761. type: MessageType.Invocation,
  762. };
  763. }
  764. else {
  765. return {
  766. arguments: args,
  767. invocationId: invocationId.toString(),
  768. target: methodName,
  769. type: MessageType.Invocation,
  770. };
  771. }
  772. }
  773. }
  774. _launchStreams(streams, promiseQueue) {
  775. if (streams.length === 0) {
  776. return;
  777. }
  778. // Synchronize stream data so they arrive in-order on the server
  779. if (!promiseQueue) {
  780. promiseQueue = Promise.resolve();
  781. }
  782. // We want to iterate over the keys, since the keys are the stream ids
  783. // eslint-disable-next-line guard-for-in
  784. for (const streamId in streams) {
  785. streams[streamId].subscribe({
  786. complete: () => {
  787. promiseQueue = promiseQueue.then(() => this._sendWithProtocol(this._createCompletionMessage(streamId)));
  788. },
  789. error: (err) => {
  790. let message;
  791. if (err instanceof Error) {
  792. message = err.message;
  793. }
  794. else if (err && err.toString) {
  795. message = err.toString();
  796. }
  797. else {
  798. message = "Unknown error";
  799. }
  800. promiseQueue = promiseQueue.then(() => this._sendWithProtocol(this._createCompletionMessage(streamId, message)));
  801. },
  802. next: (item) => {
  803. promiseQueue = promiseQueue.then(() => this._sendWithProtocol(this._createStreamItemMessage(streamId, item)));
  804. },
  805. });
  806. }
  807. }
  808. _replaceStreamingParams(args) {
  809. const streams = [];
  810. const streamIds = [];
  811. for (let i = 0; i < args.length; i++) {
  812. const argument = args[i];
  813. if (this._isObservable(argument)) {
  814. const streamId = this._invocationId;
  815. this._invocationId++;
  816. // Store the stream for later use
  817. streams[streamId] = argument;
  818. streamIds.push(streamId.toString());
  819. // remove stream from args
  820. args.splice(i, 1);
  821. }
  822. }
  823. return [streams, streamIds];
  824. }
  825. _isObservable(arg) {
  826. // This allows other stream implementations to just work (like rxjs)
  827. return arg && arg.subscribe && typeof arg.subscribe === "function";
  828. }
  829. _createStreamInvocation(methodName, args, streamIds) {
  830. const invocationId = this._invocationId;
  831. this._invocationId++;
  832. if (streamIds.length !== 0) {
  833. return {
  834. arguments: args,
  835. invocationId: invocationId.toString(),
  836. streamIds,
  837. target: methodName,
  838. type: MessageType.StreamInvocation,
  839. };
  840. }
  841. else {
  842. return {
  843. arguments: args,
  844. invocationId: invocationId.toString(),
  845. target: methodName,
  846. type: MessageType.StreamInvocation,
  847. };
  848. }
  849. }
  850. _createCancelInvocation(id) {
  851. return {
  852. invocationId: id,
  853. type: MessageType.CancelInvocation,
  854. };
  855. }
  856. _createStreamItemMessage(id, item) {
  857. return {
  858. invocationId: id,
  859. item,
  860. type: MessageType.StreamItem,
  861. };
  862. }
  863. _createCompletionMessage(id, error, result) {
  864. if (error) {
  865. return {
  866. error,
  867. invocationId: id,
  868. type: MessageType.Completion,
  869. };
  870. }
  871. return {
  872. invocationId: id,
  873. result,
  874. type: MessageType.Completion,
  875. };
  876. }
  877. }
  878. //# sourceMappingURL=HubConnection.js.map