LongPollingTransport.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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 { AbortController } from "./AbortController";
  4. import { HttpError, TimeoutError } from "./Errors";
  5. import { LogLevel } from "./ILogger";
  6. import { TransferFormat } from "./ITransport";
  7. import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
  8. // Not exported from 'index', this type is internal.
  9. /** @private */
  10. export class LongPollingTransport {
  11. constructor(httpClient, logger, options) {
  12. this._httpClient = httpClient;
  13. this._logger = logger;
  14. this._pollAbort = new AbortController();
  15. this._options = options;
  16. this._running = false;
  17. this.onreceive = null;
  18. this.onclose = null;
  19. }
  20. // This is an internal type, not exported from 'index' so this is really just internal.
  21. get pollAborted() {
  22. return this._pollAbort.aborted;
  23. }
  24. async connect(url, transferFormat) {
  25. Arg.isRequired(url, "url");
  26. Arg.isRequired(transferFormat, "transferFormat");
  27. Arg.isIn(transferFormat, TransferFormat, "transferFormat");
  28. this._url = url;
  29. this._logger.log(LogLevel.Trace, "(LongPolling transport) Connecting.");
  30. // Allow binary format on Node and Browsers that support binary content (indicated by the presence of responseType property)
  31. if (transferFormat === TransferFormat.Binary &&
  32. (typeof XMLHttpRequest !== "undefined" && typeof new XMLHttpRequest().responseType !== "string")) {
  33. throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
  34. }
  35. const [name, value] = getUserAgentHeader();
  36. const headers = { [name]: value, ...this._options.headers };
  37. const pollOptions = {
  38. abortSignal: this._pollAbort.signal,
  39. headers,
  40. timeout: 100000,
  41. withCredentials: this._options.withCredentials,
  42. };
  43. if (transferFormat === TransferFormat.Binary) {
  44. pollOptions.responseType = "arraybuffer";
  45. }
  46. // Make initial long polling request
  47. // Server uses first long polling request to finish initializing connection and it returns without data
  48. const pollUrl = `${url}&_=${Date.now()}`;
  49. this._logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}.`);
  50. const response = await this._httpClient.get(pollUrl, pollOptions);
  51. if (response.statusCode !== 200) {
  52. this._logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}.`);
  53. // Mark running as false so that the poll immediately ends and runs the close logic
  54. this._closeError = new HttpError(response.statusText || "", response.statusCode);
  55. this._running = false;
  56. }
  57. else {
  58. this._running = true;
  59. }
  60. this._receiving = this._poll(this._url, pollOptions);
  61. }
  62. async _poll(url, pollOptions) {
  63. try {
  64. while (this._running) {
  65. try {
  66. const pollUrl = `${url}&_=${Date.now()}`;
  67. this._logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}.`);
  68. const response = await this._httpClient.get(pollUrl, pollOptions);
  69. if (response.statusCode === 204) {
  70. this._logger.log(LogLevel.Information, "(LongPolling transport) Poll terminated by server.");
  71. this._running = false;
  72. }
  73. else if (response.statusCode !== 200) {
  74. this._logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}.`);
  75. // Unexpected status code
  76. this._closeError = new HttpError(response.statusText || "", response.statusCode);
  77. this._running = false;
  78. }
  79. else {
  80. // Process the response
  81. if (response.content) {
  82. this._logger.log(LogLevel.Trace, `(LongPolling transport) data received. ${getDataDetail(response.content, this._options.logMessageContent)}.`);
  83. if (this.onreceive) {
  84. this.onreceive(response.content);
  85. }
  86. }
  87. else {
  88. // This is another way timeout manifest.
  89. this._logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
  90. }
  91. }
  92. }
  93. catch (e) {
  94. if (!this._running) {
  95. // Log but disregard errors that occur after stopping
  96. this._logger.log(LogLevel.Trace, `(LongPolling transport) Poll errored after shutdown: ${e.message}`);
  97. }
  98. else {
  99. if (e instanceof TimeoutError) {
  100. // Ignore timeouts and reissue the poll.
  101. this._logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
  102. }
  103. else {
  104. // Close the connection with the error as the result.
  105. this._closeError = e;
  106. this._running = false;
  107. }
  108. }
  109. }
  110. }
  111. }
  112. finally {
  113. this._logger.log(LogLevel.Trace, "(LongPolling transport) Polling complete.");
  114. // We will reach here with pollAborted==false when the server returned a response causing the transport to stop.
  115. // If pollAborted==true then client initiated the stop and the stop method will raise the close event after DELETE is sent.
  116. if (!this.pollAborted) {
  117. this._raiseOnClose();
  118. }
  119. }
  120. }
  121. async send(data) {
  122. if (!this._running) {
  123. return Promise.reject(new Error("Cannot send until the transport is connected"));
  124. }
  125. return sendMessage(this._logger, "LongPolling", this._httpClient, this._url, data, this._options);
  126. }
  127. async stop() {
  128. this._logger.log(LogLevel.Trace, "(LongPolling transport) Stopping polling.");
  129. // Tell receiving loop to stop, abort any current request, and then wait for it to finish
  130. this._running = false;
  131. this._pollAbort.abort();
  132. try {
  133. await this._receiving;
  134. // Send DELETE to clean up long polling on the server
  135. this._logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this._url}.`);
  136. const headers = {};
  137. const [name, value] = getUserAgentHeader();
  138. headers[name] = value;
  139. const deleteOptions = {
  140. headers: { ...headers, ...this._options.headers },
  141. timeout: this._options.timeout,
  142. withCredentials: this._options.withCredentials,
  143. };
  144. await this._httpClient.delete(this._url, deleteOptions);
  145. this._logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request sent.");
  146. }
  147. finally {
  148. this._logger.log(LogLevel.Trace, "(LongPolling transport) Stop finished.");
  149. // Raise close event here instead of in polling
  150. // It needs to happen after the DELETE request is sent
  151. this._raiseOnClose();
  152. }
  153. }
  154. _raiseOnClose() {
  155. if (this.onclose) {
  156. let logMessage = "(LongPolling transport) Firing onclose event.";
  157. if (this._closeError) {
  158. logMessage += " Error: " + this._closeError;
  159. }
  160. this._logger.log(LogLevel.Trace, logMessage);
  161. this.onclose(this._closeError);
  162. }
  163. }
  164. }
  165. //# sourceMappingURL=LongPollingTransport.js.map