Fallback for TS client

This commit is contained in:
Mikael Mengistu 2018-03-27 00:18:36 +00:00 committed by GitHub
parent 79b51ad642
commit cc52beec17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 20 deletions

View File

@ -544,6 +544,33 @@ describe("hubConnection", () => {
}
});
it("transport falls back from WebSockets to SSE or LongPolling", async (done) => {
// Replace Websockets with a function that just
// throws to force fallback.
const oldWebSocket = (window as any).WebSocket;
(window as any).WebSocket = () => {
throw new Error("Kick rocks");
};
const hubConnection = new HubConnection(TESTHUBENDPOINT_URL, {
logger: LogLevel.Trace,
protocol: new JsonHubProtocol(),
});
try {
await hubConnection.start();
// Make sure that we connect with SSE or LongPolling after Websockets fail
const transportName = await hubConnection.invoke("GetActiveTransportName");
expect(transportName === "ServerSentEvents" || transportName === "LongPolling").toBe(true);
} catch (e) {
fail(e);
} finally {
(window as any).WebSocket = oldWebSocket;
done();
}
});
function getJwtToken(url): Promise<string> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

View File

@ -134,6 +134,24 @@ describe("HttpConnection", () => {
done();
});
it("start throws after all transports fail", async (done) => {
const options: IHttpConnectionOptions = {
httpClient: new TestHttpClient()
.on("POST", (r) => ({ connectionId: "42", availableTransports: [] }))
.on("GET", (r) => { throw new Error("fail"); }),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org?q=myData", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
expect(e.message).toBe("Unable to initialize any of the available transports.");
}
done();
});
it("preserves user's query string", async (done) => {
let connectUrl: string;
const fakeTransport: ITransport = {

View File

@ -79,39 +79,29 @@ export class HttpConnection implements IConnection {
// No need to add a connection ID in this case
this.url = this.baseUrl;
this.transport = this.constructTransport(TransportType.WebSockets);
// We should just call connect directly in this case.
// No fallback or negotiate in this case.
await this.transport.connect(this.url, transferFormat, this);
} else {
let headers;
const token = this.options.accessTokenFactory();
let headers;
if (token) {
headers = {
["Authorization"]: `Bearer ${token}`,
};
}
const negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), {
content: "",
headers,
});
const negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload.content as string);
this.connectionId = negotiateResponse.connectionId;
const negotiateResponse = await this.getNegotiationResponse(headers);
// the user tries to stop the the connection when it is being started
if (this.connectionState === ConnectionState.Disconnected) {
return;
}
if (this.connectionId) {
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
this.transport = this.createTransport(this.options.transport, negotiateResponse.availableTransports, transferFormat);
}
await this.createTransport(this.options.transport, negotiateResponse, transferFormat, headers);
}
this.transport.onreceive = this.onreceive;
this.transport.onclose = (e) => this.stopConnection(true, e);
await this.transport.connect(this.url, transferFormat, this);
// 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);
@ -123,16 +113,51 @@ export class HttpConnection implements IConnection {
}
}
private createTransport(requestedTransport: TransportType | ITransport, availableTransports: IAvailableTransport[], requestedTransferFormat: TransferFormat): ITransport {
private async getNegotiationResponse(headers: any): Promise<INegotiateResponse> {
const response = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), {
content: "",
headers,
});
return JSON.parse(response.content as string);
}
private updateConnectionId(negotiateResponse: INegotiateResponse) {
this.connectionId = negotiateResponse.connectionId;
this.url = this.baseUrl + (this.baseUrl.indexOf("?") === -1 ? "?" : "&") + `id=${this.connectionId}`;
}
private async createTransport(requestedTransport: TransportType | ITransport, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat, headers: any): Promise<void> {
this.updateConnectionId(negotiateResponse);
if (this.isITransport(requestedTransport)) {
this.logger.log(LogLevel.Trace, "Connection was provided an instance of ITransport, using that directly.");
return requestedTransport;
this.transport = requestedTransport;
await this.transport.connect(this.url, requestedTransferFormat, this);
// 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);
return;
}
for (const endpoint of availableTransports) {
const transports = negotiateResponse.availableTransports;
for (const endpoint of transports) {
this.connectionState = ConnectionState.Connecting;
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat);
if (typeof transport === "number") {
return this.constructTransport(transport);
this.transport = this.constructTransport(transport);
if (negotiateResponse.connectionId === null) {
negotiateResponse = await this.getNegotiationResponse(headers);
this.updateConnectionId(negotiateResponse);
}
try {
await this.transport.connect(this.url, requestedTransferFormat, this);
this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
return;
} catch (ex) {
this.logger.log(LogLevel.Error, `Failed to start the transport' ${TransportType[transport]}:' transport'${ex}'`);
this.connectionState = ConnectionState.Disconnected;
negotiateResponse.connectionId = null;
}
}
}