HubConnection.js 42 KB

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