Fixing start/stop race in the TS client

This commit is contained in:
moozzyk 2017-03-29 17:24:51 -07:00
parent 370df2d6d9
commit 841ceb24b6
4 changed files with 91 additions and 25 deletions

View File

@ -3,6 +3,7 @@ import { Connection } from "../Microsoft.AspNetCore.SignalR.Client.TS/Connection
import { ISignalROptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/ISignalROptions" import { ISignalROptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/ISignalROptions"
describe("Connection", () => { describe("Connection", () => {
it("starting connection fails if getting id fails", async (done) => { it("starting connection fails if getting id fails", async (done) => {
let options: ISignalROptions = { let options: ISignalROptions = {
httpClient: <IHttpClient>{ httpClient: <IHttpClient>{
@ -93,4 +94,32 @@ describe("Connection", () => {
done(); done();
} }
}); });
it("can stop a starting connection", async (done) => {
let options: ISignalROptions = {
httpClient: <IHttpClient>{
get(url: string): Promise<string> {
connection.stop();
return Promise.resolve("");
}
}
} as ISignalROptions;
var connection = new Connection("http://tempuri.org", undefined, options);
try {
await connection.start();
done();
}
catch (e) {
fail();
done();
}
});
it("can stop a non-started connection", async (done) => {
var connection = new Connection("http://tempuri.org");
await connection.stop();
done();
});
}); });

View File

@ -18,6 +18,7 @@ export class Connection implements IConnection {
private connectionId: string; private connectionId: string;
private httpClient: IHttpClient; private httpClient: IHttpClient;
private transport: ITransport; private transport: ITransport;
private startPromise: Promise<void>;
constructor(url: string, queryString: string = "", options: ISignalROptions = {}) { constructor(url: string, queryString: string = "", options: ISignalROptions = {}) {
this.url = url; this.url = url;
@ -28,20 +29,33 @@ export class Connection implements IConnection {
async start(transportType: TransportType = TransportType.WebSockets): Promise<void> { async start(transportType: TransportType = TransportType.WebSockets): Promise<void> {
if (this.connectionState != ConnectionState.Initial) { if (this.connectionState != ConnectionState.Initial) {
throw 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 'Initial' state."));
} }
this.connectionState = ConnectionState.Connecting; this.connectionState = ConnectionState.Connecting;
this.transport = this.createTransport(transportType); this.startPromise = this.startInternal(transportType);
this.transport.onDataReceived = this.onDataReceived; return this.startPromise;
this.transport.onClosed = e => this.stopConnection(e); }
private async startInternal(transportType: TransportType): Promise<void> {
try { try {
this.connectionId = await this.httpClient.get(`${this.url}/negotiate?${this.queryString}`); this.connectionId = await this.httpClient.get(`${this.url}/negotiate?${this.queryString}`);
// the user tries to stop the the connection when it is being started
if (this.connectionState == ConnectionState.Disconnected) {
return;
}
this.queryString = `id=${this.connectionId}`; this.queryString = `id=${this.connectionId}`;
this.transport = this.createTransport(transportType);
this.transport.onDataReceived = this.onDataReceived;
this.transport.onClosed = e => this.stopConnection(true, e);
await this.transport.connect(this.url, this.queryString); await this.transport.connect(this.url, this.queryString);
this.connectionState = ConnectionState.Connected; // only change the state if we were connecting to not overwrite
// the state if the connection is already marked as Disconnected
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
} }
catch(e) { catch(e) {
console.log("Failed to start the connection. " + e) console.log("Failed to start the connection. " + e)
@ -65,27 +79,44 @@ export class Connection implements IConnection {
throw new Error("No valid transports requested."); throw new Error("No valid transports requested.");
} }
private changeState(from: ConnectionState, to: ConnectionState): Boolean {
if (this.connectionState == from) {
this.connectionState = to;
return true;
}
return false;
}
send(data: any): Promise<void> { send(data: any): Promise<void> {
if (this.connectionState != ConnectionState.Connected) { if (this.connectionState != ConnectionState.Connected) {
throw new Error("Cannot send data if the connection is not in the 'Connected' State"); throw new Error("Cannot send data if the connection is not in the 'Connected' State");
} }
return this.transport.send(data); return this.transport.send(data);
} }
stop(): void { async stop(): Promise<void> {
if (this.connectionState != ConnectionState.Connected) { let previousState = this.connectionState;
throw new Error("Cannot stop the connection if it is not in the 'Connected' State");
}
this.stopConnection();
}
private stopConnection(error?: any) {
this.transport.stop();
this.transport = null;
this.connectionState = ConnectionState.Disconnected; this.connectionState = ConnectionState.Disconnected;
if (this.onClosed) { try {
await this.startPromise;
}
catch (e) {
// this exception is returned to the user as a rejected Promise from the start method
}
this.stopConnection(/*raiseClosed*/ previousState == ConnectionState.Connected);
}
private stopConnection(raiseClosed: Boolean, error?: any) {
if (this.transport) {
this.transport.stop();
this.transport = null;
}
this.connectionState = ConnectionState.Disconnected;
if (raiseClosed && this.onClosed) {
this.onClosed(error); this.onClosed(error);
} }
} }

View File

@ -44,9 +44,11 @@ describe('hubConnection', () => {
expect(e.message).toBe(errorMessage); expect(e.message).toBe(errorMessage);
}) })
.then(() => { .then(() => {
hubConnection.stop(); return hubConnection.stop();
done();
}) })
.then(() => {
done();
});
}) })
.catch(() => { .catch(() => {
fail(); fail();
@ -70,7 +72,9 @@ describe('hubConnection', () => {
return Promise.all([client.invoke('InvokeWithString', message), callbackPromise]); return Promise.all([client.invoke('InvokeWithString', message), callbackPromise]);
}) })
.then(() => { .then(() => {
stop(); return stop();
})
.then(() => {
done(); done();
}) })
.catch(e => { .catch(e => {

View File

@ -117,14 +117,16 @@ click('connect', event => {
isConnected = true; isConnected = true;
addLine('Connected successfully', 'green'); addLine('Connected successfully', 'green');
}) })
.catch(err => { .catch(err => {
addLine(err, 'red'); addLine(err, 'red');
}); });
}); });
click('disconnect', event => { click('disconnect', event => {
connection.stop(); connection.stop()
isConnected = false; .then(() => {
isConnected = false;
});
}); });
click('broadcast', event => { click('broadcast', event => {