Fixing start/stop race in the TS client
This commit is contained in:
parent
370df2d6d9
commit
841ceb24b6
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue