diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts index 2d7f54fe79..d62712ef1c 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts @@ -57,7 +57,7 @@ describe("Connection", () => { done(); }) .catch((error: Error) => { - expect(error.message).toBe("Cannot start a connection that is not in the 'Initial' state."); + expect(error.message).toBe("Cannot start a connection that is not in the 'Disconnected' state."); done(); }); @@ -81,11 +81,13 @@ describe("Connection", () => { } }); - it("cannot start a stopped connection", async (done) => { + it("can start a stopped connection", async (done) => { + let negotiateCalls = 0; let options: IHttpConnectionOptions = { httpClient: { post(url: string): Promise { - return Promise.reject("error"); + negotiateCalls += 1; + return Promise.reject("reached negotiate"); }, get(url: string): Promise { return Promise.resolve(""); @@ -97,22 +99,18 @@ describe("Connection", () => { let connection = new HttpConnection("http://tempuri.org", options); try { - // start will fail and transition the connection to the Disconnected state await connection.start(); - } - catch (e) { - // The connection is not setup to be running so just ignore the error. + } catch (e) { + expect(e).toBe("reached negotiate"); } try { await connection.start(); - fail(); - done(); - } - catch (e) { - expect(e.message).toBe("Cannot start a connection that is not in the 'Initial' state."); - done(); + } catch (e) { + expect(e).toBe("reached negotiate"); } + + done(); }); it("can stop a starting connection", async (done) => { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts index f67f9049ec..ec6925025e 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts @@ -10,7 +10,6 @@ import { ILogger, LogLevel } from "./ILogger" import { LoggerFactory } from "./Loggers" const enum ConnectionState { - Initial, Connecting, Connected, Disconnected @@ -23,6 +22,7 @@ interface INegotiateResponse { export class HttpConnection implements IConnection { private connectionState: ConnectionState; + private baseUrl: string; private url: string; private readonly httpClient: IHttpClient; private readonly logger: ILogger; @@ -35,16 +35,16 @@ export class HttpConnection implements IConnection { constructor(url: string, options: IHttpConnectionOptions = {}) { this.logger = LoggerFactory.createLogger(options.logging); - this.url = this.resolveUrl(url); + this.baseUrl = this.resolveUrl(url); options = options || {}; this.httpClient = options.httpClient || new HttpClient(); - this.connectionState = ConnectionState.Initial; + this.connectionState = ConnectionState.Disconnected; this.options = options; } async start(): Promise { - if (this.connectionState != ConnectionState.Initial) { - return Promise.reject(new Error("Cannot start a connection that is not in the 'Initial' state.")); + if (this.connectionState !== ConnectionState.Disconnected) { + return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state.")); } this.connectionState = ConnectionState.Connecting; @@ -56,6 +56,8 @@ export class HttpConnection implements IConnection { private async startInternal(): Promise { try { if (this.options.transport === TransportType.WebSockets) { + // No need to add a connection ID in this case + this.url = this.baseUrl; this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]); } else { @@ -65,7 +67,7 @@ export class HttpConnection implements IConnection { headers.set("Authorization", `Bearer ${this.options.jwtBearer()}`); } - let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.url), "", headers); + let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), "", headers); let negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload); this.connectionId = negotiateResponse.connectionId; @@ -76,7 +78,7 @@ export class HttpConnection implements IConnection { } if (this.connectionId) { - this.url += (this.url.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`; + this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`; this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports); } } @@ -125,7 +127,7 @@ export class HttpConnection implements IConnection { } private isITransport(transport: any): transport is ITransport { - return typeof(transport) === "object" && "connect" in transport; + return typeof (transport) === "object" && "connect" in transport; } private changeState(from: ConnectionState, to: ConnectionState): Boolean { @@ -144,7 +146,7 @@ export class HttpConnection implements IConnection { return this.transport.send(data); } - async stop(error? : Error): Promise { + async stop(error?: Error): Promise { let previousState = this.connectionState; this.connectionState = ConnectionState.Disconnected; @@ -170,7 +172,7 @@ export class HttpConnection implements IConnection { } } - private resolveUrl(url: string) : string { + private resolveUrl(url: string): string { // startsWith is not supported in IE if (url.lastIndexOf("https://", 0) === 0 || url.lastIndexOf("http://", 0) === 0) { return url; @@ -198,7 +200,7 @@ export class HttpConnection implements IConnection { private resolveNegotiateUrl(url: string): string { let index = url.indexOf("?"); - let negotiateUrl = this.url.substring(0, index === -1 ? url.length : index); + let negotiateUrl = url.substring(0, index === -1 ? url.length : index); if (negotiateUrl[negotiateUrl.length - 1] !== "/") { negotiateUrl += "/"; } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts index 716cef0abb..d62711bc10 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts @@ -154,6 +154,9 @@ export class HubConnection { } stop(): void { + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + } return this.connection.stop(); } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html index f1e89e62a0..34aa3db9bc 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/connectionTests.html @@ -1,6 +1,6 @@ - - + + @@ -22,15 +22,13 @@ return decodeURIComponent(results[2].replace(/\+/g, " ")); } - var minified = getParameterByName('debug') !== 'true' ? '.min' : ''; - if (typeof Promise === 'undefined') - { + var minified = getParameterByName('release') === 'true' ? '.min' : ''; + if (typeof Promise === 'undefined') { document.write( ' + + diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js index a6d4338395..6cf9d58a8e 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js @@ -255,16 +255,75 @@ describe('hubConnection', function () { done(); }); - hubConnection.start().then(function () { - return hubConnection.invoke('InvokeWithString', message); + hubConnection.start() + .then(function () { + return hubConnection.invoke('InvokeWithString', message); + }) + .then(function () { + return hubConnection.stop(); + }) + .catch(function (e) { + fail(e); + done(); + }); + }); + + it('can receive server calls without rebinding handler when restarted', function (done) { + var options = { + transport: transportType, + protocol: protocol, + logging: signalR.LogLevel.Trace + }; + var hubConnection = new signalR.HubConnection(TESTHUBENDPOINT_URL, options); + + var message = '你好 SignalR!'; + + // client side method names are case insensitive + var methodName = 'message'; + var idx = Math.floor(Math.random() * (methodName.length - 1)); + methodName = methodName.substr(0, idx) + methodName[idx].toUpperCase() + methodName.substr(idx + 1); + + let closeCount = 0; + let invocationCount = 0; + + hubConnection.onclose(function (e) { + expect(e).toBeUndefined(); + closeCount += 1; + if (closeCount === 1) { + // Reconnect + hubConnection.start() + .then(function () { + return hubConnection.invoke('InvokeWithString', message); + }) + .then(function () { + return hubConnection.stop(); + }) + .catch(function (e) { + fail(e); + done(); + }); + } else { + expect(invocationCount).toBe(2); + done(); + } }) - .then(function () { - return hubConnection.stop(); - }) - .catch(function (e) { - fail(e); - done(); + + hubConnection.on(methodName, function (msg) { + expect(msg).toBe(message); + invocationCount += 1; }); + + hubConnection.start() + .then(function () { + return hubConnection.invoke('InvokeWithString', message); + }) + .then(function () { + return hubConnection.stop(); + }) + .catch(function (e) { + fail(e); + done(); + }); }); it('closed with error if hub cannot be created', function (done) { @@ -312,31 +371,80 @@ describe('hubConnection', function () { : new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00]) }; - hubConnection.start().then(function () { - return hubConnection.invoke('EchoComplexObject', complexObject); - }) - .then(function (value) { - if (protocol.name === "messagepack") { - // msgpack creates a Buffer for byte arrays and jasmine fails to compare a Buffer - // and a Uint8Array even though Buffer instances are also Uint8Array instances - value.ByteArray = new Uint8Array(value.ByteArray); + hubConnection.start() + .then(function () { + return hubConnection.invoke('EchoComplexObject', complexObject); + }) + .then(function (value) { + if (protocol.name === "messagepack") { + // msgpack creates a Buffer for byte arrays and jasmine fails to compare a Buffer + // and a Uint8Array even though Buffer instances are also Uint8Array instances + value.ByteArray = new Uint8Array(value.ByteArray); - // GUIDs are serialized as raw type which is a string containing bytes which need to - // be extracted. Note that with msgpack5 the original bytes will be encoded with utf8 - // and needs to be decoded. To not go into utf8 encoding intricacies the test uses values - // less than 0x80. - let guidBytes = []; - for (let i = 0; i < value.GUID.length; i++) { - guidBytes.push(value.GUID.charCodeAt(i)); + // GUIDs are serialized as raw type which is a string containing bytes which need to + // be extracted. Note that with msgpack5 the original bytes will be encoded with utf8 + // and needs to be decoded. To not go into utf8 encoding intricacies the test uses values + // less than 0x80. + let guidBytes = []; + for (let i = 0; i < value.GUID.length; i++) { + guidBytes.push(value.GUID.charCodeAt(i)); + } + value.GUID = new Uint8Array(guidBytes); } - value.GUID = new Uint8Array(guidBytes); + expect(value).toEqual(complexObject); + }) + .then(function () { + hubConnection.stop(); + }) + .catch(function (e) { + fail(e); + done(); + }); + }); + + it('can be restarted', function (done) { + var message = '你好,世界!'; + + var options = { + transport: transportType, + protocol: protocol, + logging: signalR.LogLevel.Trace + }; + var hubConnection = new signalR.HubConnection(TESTHUBENDPOINT_URL, options); + + let closeCount = 0; + hubConnection.onclose(function (error) { + expect(error).toBe(undefined); + + // Start and invoke again + if (closeCount === 0) { + closeCount += 1; + hubConnection.start().then(function () { + hubConnection.invoke('Echo', message).then(function (result) { + expect(result).toBe(message); + }).catch(function (e) { + fail(e); + }).then(function () { + hubConnection.stop() + }); + }).catch(function (e) { + fail(e); + done(); + }); + } else { + done(); } - expect(value).toEqual(complexObject); - }) - .then(function () { - hubConnection.stop(); - }) - .catch(function (e) { + }); + + hubConnection.start().then(function () { + hubConnection.invoke('Echo', message).then(function (result) { + expect(result).toBe(message); + }).catch(function (e) { + fail(e); + }).then(function () { + hubConnection.stop() + }); + }).catch(function (e) { fail(e); done(); });