fix #1171 by teaching HttpConnection to be restartable in TypeScript (#1211)

This commit is contained in:
Andrew Stanton-Nurse 2017-12-14 14:59:45 -08:00 committed by GitHub
parent 31ef3c49df
commit d2c1138429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 61 deletions

View File

@ -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: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.reject("error");
negotiateCalls += 1;
return Promise.reject("reached negotiate");
},
get(url: string): Promise<string> {
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) => {

View File

@ -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<void> {
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<void> {
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<void> {
async stop(error?: Error): Promise<void> {
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 += "/";
}

View File

@ -154,6 +154,9 @@ export class HubConnection {
}
stop(): void {
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
}
return this.connection.stop();
}

View File

@ -1,6 +1,6 @@

<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
@ -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(
'<script type="text/javascript" src="lib/signalr/signalr-clientES5' + minified + '.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr/signalr-msgpackprotocolES5' + minified + '.js"><\/script>');
}
else
{
else {
document.write(
'<script type="text/javascript" src="lib/signalr/signalr-client' + minified + '.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr/signalr-msgpackprotocol' + minified + '.js"><\/script>');
@ -42,6 +40,8 @@
<script src="js/connectionTests.js"></script>
<script src="js/hubConnectionTests.js"></script>
</head>
<body>
</body>
</html>

View File

@ -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();
});