This commit is contained in:
parent
31ef3c49df
commit
d2c1138429
|
|
@ -57,7 +57,7 @@ describe("Connection", () => {
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.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();
|
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 = {
|
let options: IHttpConnectionOptions = {
|
||||||
httpClient: <IHttpClient>{
|
httpClient: <IHttpClient>{
|
||||||
post(url: string): Promise<string> {
|
post(url: string): Promise<string> {
|
||||||
return Promise.reject("error");
|
negotiateCalls += 1;
|
||||||
|
return Promise.reject("reached negotiate");
|
||||||
},
|
},
|
||||||
get(url: string): Promise<string> {
|
get(url: string): Promise<string> {
|
||||||
return Promise.resolve("");
|
return Promise.resolve("");
|
||||||
|
|
@ -97,22 +99,18 @@ describe("Connection", () => {
|
||||||
let connection = new HttpConnection("http://tempuri.org", options);
|
let connection = new HttpConnection("http://tempuri.org", options);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// start will fail and transition the connection to the Disconnected state
|
|
||||||
await connection.start();
|
await connection.start();
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
expect(e).toBe("reached negotiate");
|
||||||
// The connection is not setup to be running so just ignore the error.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.start();
|
await connection.start();
|
||||||
fail();
|
} catch (e) {
|
||||||
done();
|
expect(e).toBe("reached negotiate");
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
expect(e.message).toBe("Cannot start a connection that is not in the 'Initial' state.");
|
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can stop a starting connection", async (done) => {
|
it("can stop a starting connection", async (done) => {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { ILogger, LogLevel } from "./ILogger"
|
||||||
import { LoggerFactory } from "./Loggers"
|
import { LoggerFactory } from "./Loggers"
|
||||||
|
|
||||||
const enum ConnectionState {
|
const enum ConnectionState {
|
||||||
Initial,
|
|
||||||
Connecting,
|
Connecting,
|
||||||
Connected,
|
Connected,
|
||||||
Disconnected
|
Disconnected
|
||||||
|
|
@ -23,6 +22,7 @@ interface INegotiateResponse {
|
||||||
|
|
||||||
export class HttpConnection implements IConnection {
|
export class HttpConnection implements IConnection {
|
||||||
private connectionState: ConnectionState;
|
private connectionState: ConnectionState;
|
||||||
|
private baseUrl: string;
|
||||||
private url: string;
|
private url: string;
|
||||||
private readonly httpClient: IHttpClient;
|
private readonly httpClient: IHttpClient;
|
||||||
private readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
|
@ -35,16 +35,16 @@ export class HttpConnection implements IConnection {
|
||||||
|
|
||||||
constructor(url: string, options: IHttpConnectionOptions = {}) {
|
constructor(url: string, options: IHttpConnectionOptions = {}) {
|
||||||
this.logger = LoggerFactory.createLogger(options.logging);
|
this.logger = LoggerFactory.createLogger(options.logging);
|
||||||
this.url = this.resolveUrl(url);
|
this.baseUrl = this.resolveUrl(url);
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.httpClient = options.httpClient || new HttpClient();
|
this.httpClient = options.httpClient || new HttpClient();
|
||||||
this.connectionState = ConnectionState.Initial;
|
this.connectionState = ConnectionState.Disconnected;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
if (this.connectionState != ConnectionState.Initial) {
|
if (this.connectionState !== ConnectionState.Disconnected) {
|
||||||
return Promise.reject(new Error("Cannot start a connection that is not in the 'Initial' state."));
|
return Promise.reject(new Error("Cannot start a connection that is not in the 'Disconnected' state."));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectionState = ConnectionState.Connecting;
|
this.connectionState = ConnectionState.Connecting;
|
||||||
|
|
@ -56,6 +56,8 @@ export class HttpConnection implements IConnection {
|
||||||
private async startInternal(): Promise<void> {
|
private async startInternal(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (this.options.transport === TransportType.WebSockets) {
|
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]]);
|
this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -65,7 +67,7 @@ export class HttpConnection implements IConnection {
|
||||||
headers.set("Authorization", `Bearer ${this.options.jwtBearer()}`);
|
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);
|
let negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload);
|
||||||
this.connectionId = negotiateResponse.connectionId;
|
this.connectionId = negotiateResponse.connectionId;
|
||||||
|
|
@ -76,7 +78,7 @@ export class HttpConnection implements IConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.connectionId) {
|
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);
|
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 {
|
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 {
|
private changeState(from: ConnectionState, to: ConnectionState): Boolean {
|
||||||
|
|
@ -144,7 +146,7 @@ export class HttpConnection implements IConnection {
|
||||||
return this.transport.send(data);
|
return this.transport.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop(error? : Error): Promise<void> {
|
async stop(error?: Error): Promise<void> {
|
||||||
let previousState = this.connectionState;
|
let previousState = this.connectionState;
|
||||||
this.connectionState = ConnectionState.Disconnected;
|
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
|
// startsWith is not supported in IE
|
||||||
if (url.lastIndexOf("https://", 0) === 0 || url.lastIndexOf("http://", 0) === 0) {
|
if (url.lastIndexOf("https://", 0) === 0 || url.lastIndexOf("http://", 0) === 0) {
|
||||||
return url;
|
return url;
|
||||||
|
|
@ -198,7 +200,7 @@ export class HttpConnection implements IConnection {
|
||||||
|
|
||||||
private resolveNegotiateUrl(url: string): string {
|
private resolveNegotiateUrl(url: string): string {
|
||||||
let index = url.indexOf("?");
|
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] !== "/") {
|
if (negotiateUrl[negotiateUrl.length - 1] !== "/") {
|
||||||
negotiateUrl += "/";
|
negotiateUrl += "/";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,9 @@ export class HubConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
if (this.timeoutHandle) {
|
||||||
|
clearTimeout(this.timeoutHandle);
|
||||||
|
}
|
||||||
return this.connection.stop();
|
return this.connection.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
@ -22,15 +22,13 @@
|
||||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||||
}
|
}
|
||||||
|
|
||||||
var minified = getParameterByName('debug') !== 'true' ? '.min' : '';
|
var minified = getParameterByName('release') === 'true' ? '.min' : '';
|
||||||
if (typeof Promise === 'undefined')
|
if (typeof Promise === 'undefined') {
|
||||||
{
|
|
||||||
document.write(
|
document.write(
|
||||||
'<script type="text/javascript" src="lib/signalr/signalr-clientES5' + minified + '.js"><\/script>' +
|
'<script type="text/javascript" src="lib/signalr/signalr-clientES5' + minified + '.js"><\/script>' +
|
||||||
'<script type="text/javascript" src="lib/signalr/signalr-msgpackprotocolES5' + minified + '.js"><\/script>');
|
'<script type="text/javascript" src="lib/signalr/signalr-msgpackprotocolES5' + minified + '.js"><\/script>');
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
document.write(
|
document.write(
|
||||||
'<script type="text/javascript" src="lib/signalr/signalr-client' + minified + '.js"><\/script>' +
|
'<script type="text/javascript" src="lib/signalr/signalr-client' + minified + '.js"><\/script>' +
|
||||||
'<script type="text/javascript" src="lib/signalr/signalr-msgpackprotocol' + 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/connectionTests.js"></script>
|
||||||
<script src="js/hubConnectionTests.js"></script>
|
<script src="js/hubConnectionTests.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -255,16 +255,75 @@ describe('hubConnection', function () {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
hubConnection.start().then(function () {
|
hubConnection.start()
|
||||||
return hubConnection.invoke('InvokeWithString', message);
|
.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();
|
hubConnection.on(methodName, function (msg) {
|
||||||
})
|
expect(msg).toBe(message);
|
||||||
.catch(function (e) {
|
invocationCount += 1;
|
||||||
fail(e);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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) {
|
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])
|
: new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00])
|
||||||
};
|
};
|
||||||
|
|
||||||
hubConnection.start().then(function () {
|
hubConnection.start()
|
||||||
return hubConnection.invoke('EchoComplexObject', complexObject);
|
.then(function () {
|
||||||
})
|
return hubConnection.invoke('EchoComplexObject', complexObject);
|
||||||
.then(function (value) {
|
})
|
||||||
if (protocol.name === "messagepack") {
|
.then(function (value) {
|
||||||
// msgpack creates a Buffer for byte arrays and jasmine fails to compare a Buffer
|
if (protocol.name === "messagepack") {
|
||||||
// and a Uint8Array even though Buffer instances are also Uint8Array instances
|
// msgpack creates a Buffer for byte arrays and jasmine fails to compare a Buffer
|
||||||
value.ByteArray = new Uint8Array(value.ByteArray);
|
// 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
|
// 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
|
// 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
|
// and needs to be decoded. To not go into utf8 encoding intricacies the test uses values
|
||||||
// less than 0x80.
|
// less than 0x80.
|
||||||
let guidBytes = [];
|
let guidBytes = [];
|
||||||
for (let i = 0; i < value.GUID.length; i++) {
|
for (let i = 0; i < value.GUID.length; i++) {
|
||||||
guidBytes.push(value.GUID.charCodeAt(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.start().then(function () {
|
||||||
hubConnection.stop();
|
hubConnection.invoke('Echo', message).then(function (result) {
|
||||||
})
|
expect(result).toBe(message);
|
||||||
.catch(function (e) {
|
}).catch(function (e) {
|
||||||
|
fail(e);
|
||||||
|
}).then(function () {
|
||||||
|
hubConnection.stop()
|
||||||
|
});
|
||||||
|
}).catch(function (e) {
|
||||||
fail(e);
|
fail(e);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue