HttpConnection.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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.TransportSendQueue = exports.HttpConnection = void 0;
  6. const AccessTokenHttpClient_1 = require("./AccessTokenHttpClient");
  7. const DefaultHttpClient_1 = require("./DefaultHttpClient");
  8. const Errors_1 = require("./Errors");
  9. const ILogger_1 = require("./ILogger");
  10. const ITransport_1 = require("./ITransport");
  11. const LongPollingTransport_1 = require("./LongPollingTransport");
  12. const ServerSentEventsTransport_1 = require("./ServerSentEventsTransport");
  13. const Utils_1 = require("./Utils");
  14. const WebSocketTransport_1 = require("./WebSocketTransport");
  15. const MAX_REDIRECTS = 100;
  16. /** @private */
  17. class HttpConnection {
  18. constructor(url, options = {}) {
  19. this._stopPromiseResolver = () => { };
  20. this.features = {};
  21. this._negotiateVersion = 1;
  22. Utils_1.Arg.isRequired(url, "url");
  23. this._logger = Utils_1.createLogger(options.logger);
  24. this.baseUrl = this._resolveUrl(url);
  25. options = options || {};
  26. options.logMessageContent = options.logMessageContent === undefined ? false : options.logMessageContent;
  27. if (typeof options.withCredentials === "boolean" || options.withCredentials === undefined) {
  28. options.withCredentials = options.withCredentials === undefined ? true : options.withCredentials;
  29. }
  30. else {
  31. throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");
  32. }
  33. options.timeout = options.timeout === undefined ? 100 * 1000 : options.timeout;
  34. let webSocketModule = null;
  35. let eventSourceModule = null;
  36. if (Utils_1.Platform.isNode && typeof require !== "undefined") {
  37. // In order to ignore the dynamic require in webpack builds we need to do this magic
  38. // @ts-ignore: TS doesn't know about these names
  39. const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;
  40. webSocketModule = requireFunc("ws");
  41. eventSourceModule = requireFunc("eventsource");
  42. }
  43. if (!Utils_1.Platform.isNode && typeof WebSocket !== "undefined" && !options.WebSocket) {
  44. options.WebSocket = WebSocket;
  45. }
  46. else if (Utils_1.Platform.isNode && !options.WebSocket) {
  47. if (webSocketModule) {
  48. options.WebSocket = webSocketModule;
  49. }
  50. }
  51. if (!Utils_1.Platform.isNode && typeof EventSource !== "undefined" && !options.EventSource) {
  52. options.EventSource = EventSource;
  53. }
  54. else if (Utils_1.Platform.isNode && !options.EventSource) {
  55. if (typeof eventSourceModule !== "undefined") {
  56. options.EventSource = eventSourceModule;
  57. }
  58. }
  59. this._httpClient = new AccessTokenHttpClient_1.AccessTokenHttpClient(options.httpClient || new DefaultHttpClient_1.DefaultHttpClient(this._logger), options.accessTokenFactory);
  60. this._connectionState = "Disconnected" /* Disconnected */;
  61. this._connectionStarted = false;
  62. this._options = options;
  63. this.onreceive = null;
  64. this.onclose = null;
  65. }
  66. async start(transferFormat) {
  67. transferFormat = transferFormat || ITransport_1.TransferFormat.Binary;
  68. Utils_1.Arg.isIn(transferFormat, ITransport_1.TransferFormat, "transferFormat");
  69. this._logger.log(ILogger_1.LogLevel.Debug, `Starting connection with transfer format '${ITransport_1.TransferFormat[transferFormat]}'.`);
  70. if (this._connectionState !== "Disconnected" /* Disconnected */) {
  71. return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));
  72. }
  73. this._connectionState = "Connecting" /* Connecting */;
  74. this._startInternalPromise = this._startInternal(transferFormat);
  75. await this._startInternalPromise;
  76. // The TypeScript compiler thinks that connectionState must be Connecting here. The TypeScript compiler is wrong.
  77. if (this._connectionState === "Disconnecting" /* Disconnecting */) {
  78. // stop() was called and transitioned the client into the Disconnecting state.
  79. const message = "Failed to start the HttpConnection before stop() was called.";
  80. this._logger.log(ILogger_1.LogLevel.Error, message);
  81. // We cannot await stopPromise inside startInternal since stopInternal awaits the startInternalPromise.
  82. await this._stopPromise;
  83. return Promise.reject(new Errors_1.AbortError(message));
  84. }
  85. else if (this._connectionState !== "Connected" /* Connected */) {
  86. // stop() was called and transitioned the client into the Disconnecting state.
  87. const message = "HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";
  88. this._logger.log(ILogger_1.LogLevel.Error, message);
  89. return Promise.reject(new Errors_1.AbortError(message));
  90. }
  91. this._connectionStarted = true;
  92. }
  93. send(data) {
  94. if (this._connectionState !== "Connected" /* Connected */) {
  95. return Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State."));
  96. }
  97. if (!this._sendQueue) {
  98. this._sendQueue = new TransportSendQueue(this.transport);
  99. }
  100. // Transport will not be null if state is connected
  101. return this._sendQueue.send(data);
  102. }
  103. async stop(error) {
  104. if (this._connectionState === "Disconnected" /* Disconnected */) {
  105. this._logger.log(ILogger_1.LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnected state.`);
  106. return Promise.resolve();
  107. }
  108. if (this._connectionState === "Disconnecting" /* Disconnecting */) {
  109. this._logger.log(ILogger_1.LogLevel.Debug, `Call to HttpConnection.stop(${error}) ignored because the connection is already in the disconnecting state.`);
  110. return this._stopPromise;
  111. }
  112. this._connectionState = "Disconnecting" /* Disconnecting */;
  113. this._stopPromise = new Promise((resolve) => {
  114. // Don't complete stop() until stopConnection() completes.
  115. this._stopPromiseResolver = resolve;
  116. });
  117. // stopInternal should never throw so just observe it.
  118. await this._stopInternal(error);
  119. await this._stopPromise;
  120. }
  121. async _stopInternal(error) {
  122. // Set error as soon as possible otherwise there is a race between
  123. // the transport closing and providing an error and the error from a close message
  124. // We would prefer the close message error.
  125. this._stopError = error;
  126. try {
  127. await this._startInternalPromise;
  128. }
  129. catch (e) {
  130. // This exception is returned to the user as a rejected Promise from the start method.
  131. }
  132. // The transport's onclose will trigger stopConnection which will run our onclose event.
  133. // The transport should always be set if currently connected. If it wasn't set, it's likely because
  134. // stop was called during start() and start() failed.
  135. if (this.transport) {
  136. try {
  137. await this.transport.stop();
  138. }
  139. catch (e) {
  140. this._logger.log(ILogger_1.LogLevel.Error, `HttpConnection.transport.stop() threw error '${e}'.`);
  141. this._stopConnection();
  142. }
  143. this.transport = undefined;
  144. }
  145. else {
  146. this._logger.log(ILogger_1.LogLevel.Debug, "HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.");
  147. }
  148. }
  149. async _startInternal(transferFormat) {
  150. // Store the original base url and the access token factory since they may change
  151. // as part of negotiating
  152. let url = this.baseUrl;
  153. this._accessTokenFactory = this._options.accessTokenFactory;
  154. this._httpClient._accessTokenFactory = this._accessTokenFactory;
  155. try {
  156. if (this._options.skipNegotiation) {
  157. if (this._options.transport === ITransport_1.HttpTransportType.WebSockets) {
  158. // No need to add a connection ID in this case
  159. this.transport = this._constructTransport(ITransport_1.HttpTransportType.WebSockets);
  160. // We should just call connect directly in this case.
  161. // No fallback or negotiate in this case.
  162. await this._startTransport(url, transferFormat);
  163. }
  164. else {
  165. throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");
  166. }
  167. }
  168. else {
  169. let negotiateResponse = null;
  170. let redirects = 0;
  171. do {
  172. negotiateResponse = await this._getNegotiationResponse(url);
  173. // the user tries to stop the connection when it is being started
  174. if (this._connectionState === "Disconnecting" /* Disconnecting */ || this._connectionState === "Disconnected" /* Disconnected */) {
  175. throw new Errors_1.AbortError("The connection was stopped during negotiation.");
  176. }
  177. if (negotiateResponse.error) {
  178. throw new Error(negotiateResponse.error);
  179. }
  180. if (negotiateResponse.ProtocolVersion) {
  181. throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");
  182. }
  183. if (negotiateResponse.url) {
  184. url = negotiateResponse.url;
  185. }
  186. if (negotiateResponse.accessToken) {
  187. // Replace the current access token factory with one that uses
  188. // the returned access token
  189. const accessToken = negotiateResponse.accessToken;
  190. this._accessTokenFactory = () => accessToken;
  191. // set the factory to undefined so the AccessTokenHttpClient won't retry with the same token, since we know it won't change until a connection restart
  192. this._httpClient._accessToken = accessToken;
  193. this._httpClient._accessTokenFactory = undefined;
  194. }
  195. redirects++;
  196. } while (negotiateResponse.url && redirects < MAX_REDIRECTS);
  197. if (redirects === MAX_REDIRECTS && negotiateResponse.url) {
  198. throw new Error("Negotiate redirection limit exceeded.");
  199. }
  200. await this._createTransport(url, this._options.transport, negotiateResponse, transferFormat);
  201. }
  202. if (this.transport instanceof LongPollingTransport_1.LongPollingTransport) {
  203. this.features.inherentKeepAlive = true;
  204. }
  205. if (this._connectionState === "Connecting" /* Connecting */) {
  206. // Ensure the connection transitions to the connected state prior to completing this.startInternalPromise.
  207. // start() will handle the case when stop was called and startInternal exits still in the disconnecting state.
  208. this._logger.log(ILogger_1.LogLevel.Debug, "The HttpConnection connected successfully.");
  209. this._connectionState = "Connected" /* Connected */;
  210. }
  211. // stop() is waiting on us via this.startInternalPromise so keep this.transport around so it can clean up.
  212. // This is the only case startInternal can exit in neither the connected nor disconnected state because stopConnection()
  213. // will transition to the disconnected state. start() will wait for the transition using the stopPromise.
  214. }
  215. catch (e) {
  216. this._logger.log(ILogger_1.LogLevel.Error, "Failed to start the connection: " + e);
  217. this._connectionState = "Disconnected" /* Disconnected */;
  218. this.transport = undefined;
  219. // if start fails, any active calls to stop assume that start will complete the stop promise
  220. this._stopPromiseResolver();
  221. return Promise.reject(e);
  222. }
  223. }
  224. async _getNegotiationResponse(url) {
  225. const headers = {};
  226. const [name, value] = Utils_1.getUserAgentHeader();
  227. headers[name] = value;
  228. const negotiateUrl = this._resolveNegotiateUrl(url);
  229. this._logger.log(ILogger_1.LogLevel.Debug, `Sending negotiation request: ${negotiateUrl}.`);
  230. try {
  231. const response = await this._httpClient.post(negotiateUrl, {
  232. content: "",
  233. headers: { ...headers, ...this._options.headers },
  234. timeout: this._options.timeout,
  235. withCredentials: this._options.withCredentials,
  236. });
  237. if (response.statusCode !== 200) {
  238. return Promise.reject(new Error(`Unexpected status code returned from negotiate '${response.statusCode}'`));
  239. }
  240. const negotiateResponse = JSON.parse(response.content);
  241. if (!negotiateResponse.negotiateVersion || negotiateResponse.negotiateVersion < 1) {
  242. // Negotiate version 0 doesn't use connectionToken
  243. // So we set it equal to connectionId so all our logic can use connectionToken without being aware of the negotiate version
  244. negotiateResponse.connectionToken = negotiateResponse.connectionId;
  245. }
  246. return negotiateResponse;
  247. }
  248. catch (e) {
  249. let errorMessage = "Failed to complete negotiation with the server: " + e;
  250. if (e instanceof Errors_1.HttpError) {
  251. if (e.statusCode === 404) {
  252. errorMessage = errorMessage + " Either this is not a SignalR endpoint or there is a proxy blocking the connection.";
  253. }
  254. }
  255. this._logger.log(ILogger_1.LogLevel.Error, errorMessage);
  256. return Promise.reject(new Errors_1.FailedToNegotiateWithServerError(errorMessage));
  257. }
  258. }
  259. _createConnectUrl(url, connectionToken) {
  260. if (!connectionToken) {
  261. return url;
  262. }
  263. return url + (url.indexOf("?") === -1 ? "?" : "&") + `id=${connectionToken}`;
  264. }
  265. async _createTransport(url, requestedTransport, negotiateResponse, requestedTransferFormat) {
  266. let connectUrl = this._createConnectUrl(url, negotiateResponse.connectionToken);
  267. if (this._isITransport(requestedTransport)) {
  268. this._logger.log(ILogger_1.LogLevel.Debug, "Connection was provided an instance of ITransport, using that directly.");
  269. this.transport = requestedTransport;
  270. await this._startTransport(connectUrl, requestedTransferFormat);
  271. this.connectionId = negotiateResponse.connectionId;
  272. return;
  273. }
  274. const transportExceptions = [];
  275. const transports = negotiateResponse.availableTransports || [];
  276. let negotiate = negotiateResponse;
  277. for (const endpoint of transports) {
  278. const transportOrError = this._resolveTransportOrError(endpoint, requestedTransport, requestedTransferFormat);
  279. if (transportOrError instanceof Error) {
  280. // Store the error and continue, we don't want to cause a re-negotiate in these cases
  281. transportExceptions.push(`${endpoint.transport} failed:`);
  282. transportExceptions.push(transportOrError);
  283. }
  284. else if (this._isITransport(transportOrError)) {
  285. this.transport = transportOrError;
  286. if (!negotiate) {
  287. try {
  288. negotiate = await this._getNegotiationResponse(url);
  289. }
  290. catch (ex) {
  291. return Promise.reject(ex);
  292. }
  293. connectUrl = this._createConnectUrl(url, negotiate.connectionToken);
  294. }
  295. try {
  296. await this._startTransport(connectUrl, requestedTransferFormat);
  297. this.connectionId = negotiate.connectionId;
  298. return;
  299. }
  300. catch (ex) {
  301. this._logger.log(ILogger_1.LogLevel.Error, `Failed to start the transport '${endpoint.transport}': ${ex}`);
  302. negotiate = undefined;
  303. transportExceptions.push(new Errors_1.FailedToStartTransportError(`${endpoint.transport} failed: ${ex}`, ITransport_1.HttpTransportType[endpoint.transport]));
  304. if (this._connectionState !== "Connecting" /* Connecting */) {
  305. const message = "Failed to select transport before stop() was called.";
  306. this._logger.log(ILogger_1.LogLevel.Debug, message);
  307. return Promise.reject(new Errors_1.AbortError(message));
  308. }
  309. }
  310. }
  311. }
  312. if (transportExceptions.length > 0) {
  313. return Promise.reject(new Errors_1.AggregateErrors(`Unable to connect to the server with any of the available transports. ${transportExceptions.join(" ")}`, transportExceptions));
  314. }
  315. return Promise.reject(new Error("None of the transports supported by the client are supported by the server."));
  316. }
  317. _constructTransport(transport) {
  318. switch (transport) {
  319. case ITransport_1.HttpTransportType.WebSockets:
  320. if (!this._options.WebSocket) {
  321. throw new Error("'WebSocket' is not supported in your environment.");
  322. }
  323. return new WebSocketTransport_1.WebSocketTransport(this._httpClient, this._accessTokenFactory, this._logger, this._options.logMessageContent, this._options.WebSocket, this._options.headers || {});
  324. case ITransport_1.HttpTransportType.ServerSentEvents:
  325. if (!this._options.EventSource) {
  326. throw new Error("'EventSource' is not supported in your environment.");
  327. }
  328. return new ServerSentEventsTransport_1.ServerSentEventsTransport(this._httpClient, this._httpClient._accessToken, this._logger, this._options);
  329. case ITransport_1.HttpTransportType.LongPolling:
  330. return new LongPollingTransport_1.LongPollingTransport(this._httpClient, this._logger, this._options);
  331. default:
  332. throw new Error(`Unknown transport: ${transport}.`);
  333. }
  334. }
  335. _startTransport(url, transferFormat) {
  336. this.transport.onreceive = this.onreceive;
  337. this.transport.onclose = (e) => this._stopConnection(e);
  338. return this.transport.connect(url, transferFormat);
  339. }
  340. _resolveTransportOrError(endpoint, requestedTransport, requestedTransferFormat) {
  341. const transport = ITransport_1.HttpTransportType[endpoint.transport];
  342. if (transport === null || transport === undefined) {
  343. this._logger.log(ILogger_1.LogLevel.Debug, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`);
  344. return new Error(`Skipping transport '${endpoint.transport}' because it is not supported by this client.`);
  345. }
  346. else {
  347. if (transportMatches(requestedTransport, transport)) {
  348. const transferFormats = endpoint.transferFormats.map((s) => ITransport_1.TransferFormat[s]);
  349. if (transferFormats.indexOf(requestedTransferFormat) >= 0) {
  350. if ((transport === ITransport_1.HttpTransportType.WebSockets && !this._options.WebSocket) ||
  351. (transport === ITransport_1.HttpTransportType.ServerSentEvents && !this._options.EventSource)) {
  352. this._logger.log(ILogger_1.LogLevel.Debug, `Skipping transport '${ITransport_1.HttpTransportType[transport]}' because it is not supported in your environment.'`);
  353. return new Errors_1.UnsupportedTransportError(`'${ITransport_1.HttpTransportType[transport]}' is not supported in your environment.`, transport);
  354. }
  355. else {
  356. this._logger.log(ILogger_1.LogLevel.Debug, `Selecting transport '${ITransport_1.HttpTransportType[transport]}'.`);
  357. try {
  358. return this._constructTransport(transport);
  359. }
  360. catch (ex) {
  361. return ex;
  362. }
  363. }
  364. }
  365. else {
  366. this._logger.log(ILogger_1.LogLevel.Debug, `Skipping transport '${ITransport_1.HttpTransportType[transport]}' because it does not support the requested transfer format '${ITransport_1.TransferFormat[requestedTransferFormat]}'.`);
  367. return new Error(`'${ITransport_1.HttpTransportType[transport]}' does not support ${ITransport_1.TransferFormat[requestedTransferFormat]}.`);
  368. }
  369. }
  370. else {
  371. this._logger.log(ILogger_1.LogLevel.Debug, `Skipping transport '${ITransport_1.HttpTransportType[transport]}' because it was disabled by the client.`);
  372. return new Errors_1.DisabledTransportError(`'${ITransport_1.HttpTransportType[transport]}' is disabled by the client.`, transport);
  373. }
  374. }
  375. }
  376. _isITransport(transport) {
  377. return transport && typeof (transport) === "object" && "connect" in transport;
  378. }
  379. _stopConnection(error) {
  380. this._logger.log(ILogger_1.LogLevel.Debug, `HttpConnection.stopConnection(${error}) called while in state ${this._connectionState}.`);
  381. this.transport = undefined;
  382. // If we have a stopError, it takes precedence over the error from the transport
  383. error = this._stopError || error;
  384. this._stopError = undefined;
  385. if (this._connectionState === "Disconnected" /* Disconnected */) {
  386. this._logger.log(ILogger_1.LogLevel.Debug, `Call to HttpConnection.stopConnection(${error}) was ignored because the connection is already in the disconnected state.`);
  387. return;
  388. }
  389. if (this._connectionState === "Connecting" /* Connecting */) {
  390. this._logger.log(ILogger_1.LogLevel.Warning, `Call to HttpConnection.stopConnection(${error}) was ignored because the connection is still in the connecting state.`);
  391. throw new Error(`HttpConnection.stopConnection(${error}) was called while the connection is still in the connecting state.`);
  392. }
  393. if (this._connectionState === "Disconnecting" /* Disconnecting */) {
  394. // A call to stop() induced this call to stopConnection and needs to be completed.
  395. // Any stop() awaiters will be scheduled to continue after the onclose callback fires.
  396. this._stopPromiseResolver();
  397. }
  398. if (error) {
  399. this._logger.log(ILogger_1.LogLevel.Error, `Connection disconnected with error '${error}'.`);
  400. }
  401. else {
  402. this._logger.log(ILogger_1.LogLevel.Information, "Connection disconnected.");
  403. }
  404. if (this._sendQueue) {
  405. this._sendQueue.stop().catch((e) => {
  406. this._logger.log(ILogger_1.LogLevel.Error, `TransportSendQueue.stop() threw error '${e}'.`);
  407. });
  408. this._sendQueue = undefined;
  409. }
  410. this.connectionId = undefined;
  411. this._connectionState = "Disconnected" /* Disconnected */;
  412. if (this._connectionStarted) {
  413. this._connectionStarted = false;
  414. try {
  415. if (this.onclose) {
  416. this.onclose(error);
  417. }
  418. }
  419. catch (e) {
  420. this._logger.log(ILogger_1.LogLevel.Error, `HttpConnection.onclose(${error}) threw error '${e}'.`);
  421. }
  422. }
  423. }
  424. _resolveUrl(url) {
  425. // startsWith is not supported in IE
  426. if (url.lastIndexOf("https://", 0) === 0 || url.lastIndexOf("http://", 0) === 0) {
  427. return url;
  428. }
  429. if (!Utils_1.Platform.isBrowser) {
  430. throw new Error(`Cannot resolve '${url}'.`);
  431. }
  432. // Setting the url to the href propery of an anchor tag handles normalization
  433. // for us. There are 3 main cases.
  434. // 1. Relative path normalization e.g "b" -> "http://localhost:5000/a/b"
  435. // 2. Absolute path normalization e.g "/a/b" -> "http://localhost:5000/a/b"
  436. // 3. Networkpath reference normalization e.g "//localhost:5000/a/b" -> "http://localhost:5000/a/b"
  437. const aTag = window.document.createElement("a");
  438. aTag.href = url;
  439. this._logger.log(ILogger_1.LogLevel.Information, `Normalizing '${url}' to '${aTag.href}'.`);
  440. return aTag.href;
  441. }
  442. _resolveNegotiateUrl(url) {
  443. const index = url.indexOf("?");
  444. let negotiateUrl = url.substring(0, index === -1 ? url.length : index);
  445. if (negotiateUrl[negotiateUrl.length - 1] !== "/") {
  446. negotiateUrl += "/";
  447. }
  448. negotiateUrl += "negotiate";
  449. negotiateUrl += index === -1 ? "" : url.substring(index);
  450. if (negotiateUrl.indexOf("negotiateVersion") === -1) {
  451. negotiateUrl += index === -1 ? "?" : "&";
  452. negotiateUrl += "negotiateVersion=" + this._negotiateVersion;
  453. }
  454. return negotiateUrl;
  455. }
  456. }
  457. exports.HttpConnection = HttpConnection;
  458. function transportMatches(requestedTransport, actualTransport) {
  459. return !requestedTransport || ((actualTransport & requestedTransport) !== 0);
  460. }
  461. /** @private */
  462. class TransportSendQueue {
  463. constructor(_transport) {
  464. this._transport = _transport;
  465. this._buffer = [];
  466. this._executing = true;
  467. this._sendBufferedData = new PromiseSource();
  468. this._transportResult = new PromiseSource();
  469. this._sendLoopPromise = this._sendLoop();
  470. }
  471. send(data) {
  472. this._bufferData(data);
  473. if (!this._transportResult) {
  474. this._transportResult = new PromiseSource();
  475. }
  476. return this._transportResult.promise;
  477. }
  478. stop() {
  479. this._executing = false;
  480. this._sendBufferedData.resolve();
  481. return this._sendLoopPromise;
  482. }
  483. _bufferData(data) {
  484. if (this._buffer.length && typeof (this._buffer[0]) !== typeof (data)) {
  485. throw new Error(`Expected data to be of type ${typeof (this._buffer)} but was of type ${typeof (data)}`);
  486. }
  487. this._buffer.push(data);
  488. this._sendBufferedData.resolve();
  489. }
  490. async _sendLoop() {
  491. while (true) {
  492. await this._sendBufferedData.promise;
  493. if (!this._executing) {
  494. if (this._transportResult) {
  495. this._transportResult.reject("Connection stopped.");
  496. }
  497. break;
  498. }
  499. this._sendBufferedData = new PromiseSource();
  500. const transportResult = this._transportResult;
  501. this._transportResult = undefined;
  502. const data = typeof (this._buffer[0]) === "string" ?
  503. this._buffer.join("") :
  504. TransportSendQueue._concatBuffers(this._buffer);
  505. this._buffer.length = 0;
  506. try {
  507. await this._transport.send(data);
  508. transportResult.resolve();
  509. }
  510. catch (error) {
  511. transportResult.reject(error);
  512. }
  513. }
  514. }
  515. static _concatBuffers(arrayBuffers) {
  516. const totalLength = arrayBuffers.map((b) => b.byteLength).reduce((a, b) => a + b);
  517. const result = new Uint8Array(totalLength);
  518. let offset = 0;
  519. for (const item of arrayBuffers) {
  520. result.set(new Uint8Array(item), offset);
  521. offset += item.byteLength;
  522. }
  523. return result.buffer;
  524. }
  525. }
  526. exports.TransportSendQueue = TransportSendQueue;
  527. class PromiseSource {
  528. constructor() {
  529. this.promise = new Promise((resolve, reject) => [this._resolver, this._rejecter] = [resolve, reject]);
  530. }
  531. resolve() {
  532. this._resolver();
  533. }
  534. reject(reason) {
  535. this._rejecter(reason);
  536. }
  537. }
  538. //# sourceMappingURL=HttpConnection.js.map