@microsoft/signalr
Version:
ASP.NET Core SignalR Client
183 lines • 9.03 kB
JavaScript
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
import { AbortController } from "./AbortController";
import { HttpError, TimeoutError } from "./Errors";
import { LogLevel } from "./ILogger";
import { TransferFormat } from "./ITransport";
import { Arg, getDataDetail, getUserAgentHeader, sendMessage } from "./Utils";
// Not exported from 'index', this type is internal.
/** @private */
export class LongPollingTransport {
// This is an internal type, not exported from 'index' so this is really just internal.
get pollAborted() {
return this._pollAbort.aborted;
}
constructor(httpClient, logger, options) {
this._httpClient = httpClient;
this._logger = logger;
this._pollAbort = new AbortController();
this._options = options;
this._running = false;
this.onreceive = null;
this.onclose = null;
}
async connect(url, transferFormat) {
Arg.isRequired(url, "url");
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
this._url = url;
this._logger.log(LogLevel.Trace, "(LongPolling transport) Connecting.");
// Allow binary format on Node and Browsers that support binary content (indicated by the presence of responseType property)
if (transferFormat === TransferFormat.Binary &&
(typeof XMLHttpRequest !== "undefined" && typeof new XMLHttpRequest().responseType !== "string")) {
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
}
const [name, value] = getUserAgentHeader();
const headers = { [name]: value, ...this._options.headers };
const pollOptions = {
abortSignal: this._pollAbort.signal,
headers,
timeout: 100000,
withCredentials: this._options.withCredentials,
};
if (transferFormat === TransferFormat.Binary) {
pollOptions.responseType = "arraybuffer";
}
// Make initial long polling request
// Server uses first long polling request to finish initializing connection and it returns without data
const pollUrl = `${url}&_=${Date.now()}`;
this._logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}.`);
const response = await this._httpClient.get(pollUrl, pollOptions);
if (response.statusCode !== 200) {
this._logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}.`);
// Mark running as false so that the poll immediately ends and runs the close logic
this._closeError = new HttpError(response.statusText || "", response.statusCode);
this._running = false;
}
else {
this._running = true;
}
this._receiving = this._poll(this._url, pollOptions);
}
async _poll(url, pollOptions) {
try {
while (this._running) {
try {
const pollUrl = `${url}&_=${Date.now()}`;
this._logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}.`);
const response = await this._httpClient.get(pollUrl, pollOptions);
if (response.statusCode === 204) {
this._logger.log(LogLevel.Information, "(LongPolling transport) Poll terminated by server.");
this._running = false;
}
else if (response.statusCode !== 200) {
this._logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}.`);
// Unexpected status code
this._closeError = new HttpError(response.statusText || "", response.statusCode);
this._running = false;
}
else {
// Process the response
if (response.content) {
this._logger.log(LogLevel.Trace, `(LongPolling transport) data received. ${getDataDetail(response.content, this._options.logMessageContent)}.`);
if (this.onreceive) {
this.onreceive(response.content);
}
}
else {
// This is another way timeout manifest.
this._logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
}
}
}
catch (e) {
if (!this._running) {
// Log but disregard errors that occur after stopping
this._logger.log(LogLevel.Trace, `(LongPolling transport) Poll errored after shutdown: ${e.message}`);
}
else {
if (e instanceof TimeoutError) {
// Ignore timeouts and reissue the poll.
this._logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
}
else {
// Close the connection with the error as the result.
this._closeError = e;
this._running = false;
}
}
}
}
}
finally {
this._logger.log(LogLevel.Trace, "(LongPolling transport) Polling complete.");
// We will reach here with pollAborted==false when the server returned a response causing the transport to stop.
// If pollAborted==true then client initiated the stop and the stop method will raise the close event after DELETE is sent.
if (!this.pollAborted) {
this._raiseOnClose();
}
}
}
async send(data) {
if (!this._running) {
return Promise.reject(new Error("Cannot send until the transport is connected"));
}
return sendMessage(this._logger, "LongPolling", this._httpClient, this._url, data, this._options);
}
async stop() {
this._logger.log(LogLevel.Trace, "(LongPolling transport) Stopping polling.");
// Tell receiving loop to stop, abort any current request, and then wait for it to finish
this._running = false;
this._pollAbort.abort();
try {
await this._receiving;
// Send DELETE to clean up long polling on the server
this._logger.log(LogLevel.Trace, `(LongPolling transport) sending DELETE request to ${this._url}.`);
const headers = {};
const [name, value] = getUserAgentHeader();
headers[name] = value;
const deleteOptions = {
headers: { ...headers, ...this._options.headers },
timeout: this._options.timeout,
withCredentials: this._options.withCredentials,
};
let error;
try {
await this._httpClient.delete(this._url, deleteOptions);
}
catch (err) {
error = err;
}
if (error) {
if (error instanceof HttpError) {
if (error.statusCode === 404) {
this._logger.log(LogLevel.Trace, "(LongPolling transport) A 404 response was returned from sending a DELETE request.");
}
else {
this._logger.log(LogLevel.Trace, `(LongPolling transport) Error sending a DELETE request: ${error}`);
}
}
}
else {
this._logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request accepted.");
}
}
finally {
this._logger.log(LogLevel.Trace, "(LongPolling transport) Stop finished.");
// Raise close event here instead of in polling
// It needs to happen after the DELETE request is sent
this._raiseOnClose();
}
}
_raiseOnClose() {
if (this.onclose) {
let logMessage = "(LongPolling transport) Firing onclose event.";
if (this._closeError) {
logMessage += " Error: " + this._closeError;
}
this._logger.log(LogLevel.Trace, logMessage);
this.onclose(this._closeError);
}
}
}
//# sourceMappingURL=LongPollingTransport.js.map