diff --git a/clients/ts/FunctionalTests/ts/HubConnectionTests.ts b/clients/ts/FunctionalTests/ts/HubConnectionTests.ts index bd539851d6..0567382a29 100644 --- a/clients/ts/FunctionalTests/ts/HubConnectionTests.ts +++ b/clients/ts/FunctionalTests/ts/HubConnectionTests.ts @@ -577,13 +577,8 @@ describe("hubConnection", () => { const hubConnection = getConnectionBuilder(transportType).build(); hubConnection.serverTimeoutInMilliseconds = 100; - const timeout = setTimeout(200, () => { - fail("Server timeout did not fire within expected interval"); - }); - hubConnection.start().then(() => { hubConnection.onclose((error) => { - clearTimeout(timeout); expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server.")); done(); }); diff --git a/clients/ts/signalr/spec/LongPollingTransport.spec.ts b/clients/ts/signalr/spec/LongPollingTransport.spec.ts new file mode 100644 index 0000000000..8023ab27d4 --- /dev/null +++ b/clients/ts/signalr/spec/LongPollingTransport.spec.ts @@ -0,0 +1,67 @@ +import { HttpResponse } from "../src/HttpClient"; +import { LogLevel } from "../src/ILogger"; +import { TransferFormat } from "../src/ITransport"; +import { NullLogger } from "../src/Loggers"; +import { LongPollingTransport } from "../src/LongPollingTransport"; +import { ConsoleLogger } from "../src/Utils"; + +import { TestHttpClient } from "./TestHttpClient"; +import { asyncit as it, PromiseSource } from "./Utils"; + +describe("LongPollingTransport", () => { + it("shuts down poll after timeout even if server doesn't shut it down on receiving the DELETE", async () => { + let firstPoll = true; + const pollCompleted = new PromiseSource(); + const client = new TestHttpClient() + .on("GET", async (r) => { + if (firstPoll) { + firstPoll = false; + return new HttpResponse(200); + } else { + // Turn 'onabort' into a promise. + const abort = new Promise((resolve, reject) => r.abortSignal.onabort = resolve); + await abort; + + // Signal that the poll has completed. + pollCompleted.resolve(); + return new HttpResponse(204); + } + }) + .on("DELETE", (r) => new HttpResponse(202)); + const transport = new LongPollingTransport(client, null, NullLogger.instance, false, 100); + + await transport.connect("http://example.com", TransferFormat.Text); + await transport.stop(); + + // This should complete within the shutdown timeout + await pollCompleted.promise; + }); + + it("sends DELETE request on stop", async () => { + let firstPoll = true; + const deleteReceived = new PromiseSource(); + const pollCompleted = new PromiseSource(); + const client = new TestHttpClient() + .on("GET", async (r) => { + if (firstPoll) { + firstPoll = false; + return new HttpResponse(200); + } else { + await deleteReceived.promise; + pollCompleted.resolve(); + return new HttpResponse(204); + } + }) + .on("DELETE", (r) => { + deleteReceived.resolve(); + return new HttpResponse(202); + }); + const transport = new LongPollingTransport(client, null, NullLogger.instance, false); + + await transport.connect("http://example.com", TransferFormat.Text); + await transport.stop(); + + // This should complete, because the DELETE request triggers it to stop. + await pollCompleted.promise; + }); +}); \ No newline at end of file diff --git a/clients/ts/signalr/src/LongPollingTransport.ts b/clients/ts/signalr/src/LongPollingTransport.ts index db0c2edf5c..b2b456f8a2 100644 --- a/clients/ts/signalr/src/LongPollingTransport.ts +++ b/clients/ts/signalr/src/LongPollingTransport.ts @@ -19,15 +19,17 @@ export class LongPollingTransport implements ITransport { private url: string; private pollXhr: XMLHttpRequest; private pollAbort: AbortController; + private shutdownTimer: any; // We use 'any' because this is an object in NodeJS. But it still gets passed to clearTimeout, so it doesn't really matter private shutdownTimeout: number; private running: boolean; - constructor(httpClient: HttpClient, accessTokenFactory: () => string | Promise, logger: ILogger, logMessageContent: boolean) { + constructor(httpClient: HttpClient, accessTokenFactory: () => string | Promise, logger: ILogger, logMessageContent: boolean, shutdownTimeout?: number) { this.httpClient = httpClient; this.accessTokenFactory = accessTokenFactory || (() => null); this.logger = logger; this.pollAbort = new AbortController(); this.logMessageContent = logMessageContent; + this.shutdownTimeout = shutdownTimeout || SHUTDOWN_TIMEOUT; } public async connect(url: string, transferFormat: TransferFormat): Promise { @@ -107,7 +109,7 @@ export class LongPollingTransport implements ITransport { this.logger.log(LogLevel.Information, "(LongPolling transport) Poll terminated by server"); // If we were on a timeout waiting for shutdown, unregister it. - clearTimeout(this.shutdownTimeout); + clearTimeout(this.shutdownTimer); this.running = false; } else if (response.statusCode !== 200) { @@ -179,10 +181,10 @@ export class LongPollingTransport implements ITransport { } finally { // Abort the poll after 5 seconds if the server doesn't stop it. if (!this.pollAbort.aborted) { - this.shutdownTimeout = setTimeout(SHUTDOWN_TIMEOUT, () => { - this.logger.log(LogLevel.Warning, "(LongPolling transport) server did not terminate within 5 seconds after DELETE request, cancelling poll."); + this.shutdownTimer = setTimeout(() => { + this.logger.log(LogLevel.Warning, "(LongPolling transport) server did not terminate after DELETE request, canceling poll."); this.pollAbort.abort(); - }); + }, this.shutdownTimeout); } } }