Refactor HttpClient and use it in LongPollingTransport (#1243)

This commit is contained in:
Andrew Stanton-Nurse 2018-01-02 16:49:02 -08:00 committed by GitHub
parent 9a128b42ef
commit 44052ffcf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 557 additions and 289 deletions

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { asyncit as it } from "./Utils";
import { AbortController } from "../Microsoft.AspNetCore.SignalR.Client.TS/AbortController";
describe("AbortSignal", () => {
describe("aborted", () => {
it("is false on initialization", () => {
expect(new AbortController().signal.aborted).toBe(false);
});
it("is true when aborted", () => {
let controller = new AbortController();
let signal = controller.signal;
controller.abort();
expect(signal.aborted).toBe(true);
})
});
describe("onabort", () => {
it("is called when abort is called", () => {
let controller = new AbortController();
let signal = controller.signal;
let abortCalled = false;
signal.onabort = () => abortCalled = true;
controller.abort();
expect(abortCalled).toBe(true);
})
})
});

View File

@ -0,0 +1,86 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { asyncit as it } from "./Utils"
import { TestHttpClient } from "./TestHttpClient";
import { HttpRequest } from "../Microsoft.AspNetCore.SignalR.Client.TS/index";
describe("HttpClient", () => {
describe("get", () => {
it("sets the method and URL appropriately", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.get("http://localhost");
expect(request.method).toEqual("GET");
expect(request.url).toEqual("http://localhost");
});
it("overrides method and url in options", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.get("http://localhost", {
method: "OPTIONS",
url: "http://wrong"
});
expect(request.method).toEqual("GET");
expect(request.url).toEqual("http://localhost");
})
it("copies other options", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.get("http://localhost", {
timeout: 42,
});
expect(request.timeout).toEqual(42);
})
});
describe("post", () => {
it("sets the method and URL appropriately", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.post("http://localhost");
expect(request.method).toEqual("POST");
expect(request.url).toEqual("http://localhost");
});
it("overrides method and url in options", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.post("http://localhost", {
method: "OPTIONS",
url: "http://wrong"
});
expect(request.method).toEqual("POST");
expect(request.url).toEqual("http://localhost");
})
it("copies other options", async () => {
let request: HttpRequest;
let testClient = new TestHttpClient().on(r => {
request = r; return "";
});
await testClient.post("http://localhost", {
timeout: 42,
});
expect(request.timeout).toEqual(42);
})
});
});

View File

@ -1,14 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { IHttpClient } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpClient"
import { TestHttpClient } from "./TestHttpClient"
import { HttpConnection } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection"
import { IHttpConnectionOptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions"
import { DataReceived, TransportClosed } from "../Microsoft.AspNetCore.SignalR.Client.TS/Common"
import { ITransport, TransportType, TransferMode } from "../Microsoft.AspNetCore.SignalR.Client.TS/Transports"
import { eachTransport, eachEndpointUrl } from "./Common";
import { HttpResponse } from "../Microsoft.AspNetCore.SignalR.Client.TS/index";
describe("Connection", () => {
describe("HttpConnection", () => {
it("cannot be created with relative url if document object is not present", () => {
expect(() => new HttpConnection("/test"))
.toThrow(new Error("Cannot resolve '/test'."));
@ -23,14 +24,9 @@ describe("Connection", () => {
it("starting connection fails if getting id fails", async (done) => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.reject("error");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
httpClient: new TestHttpClient()
.on("POST", r => Promise.reject("error"))
.on("GET", r => ""),
logger: null
} as IHttpConnectionOptions;
@ -49,8 +45,8 @@ describe("Connection", () => {
it("cannot start a running connection", async (done) => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
httpClient: new TestHttpClient()
.on("POST", r => {
connection.start()
.then(() => {
fail();
@ -60,13 +56,8 @@ describe("Connection", () => {
expect(error.message).toBe("Cannot start a connection that is not in the 'Disconnected' state.");
done();
});
return Promise.reject("error");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
}),
logger: null
} as IHttpConnectionOptions;
@ -84,15 +75,12 @@ describe("Connection", () => {
it("can start a stopped connection", async (done) => {
let negotiateCalls = 0;
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
httpClient: new TestHttpClient()
.on("POST", r => {
negotiateCalls += 1;
return Promise.reject("reached negotiate");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
})
.on("GET", r => ""),
logger: null
} as IHttpConnectionOptions;
@ -115,16 +103,15 @@ describe("Connection", () => {
it("can stop a starting connection", async (done) => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
httpClient: new TestHttpClient()
.on("POST", r => {
connection.stop();
return Promise.resolve("{}");
},
get(url: string): Promise<string> {
return "{}";
})
.on("GET", r => {
connection.stop();
return Promise.resolve("");
}
},
return "";
}),
logger: null
} as IHttpConnectionOptions;
@ -164,16 +151,11 @@ describe("Connection", () => {
}
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\" }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
httpClient: new TestHttpClient()
.on("POST", r => "{ \"connectionId\": \"42\" }")
.on("GET", r => ""),
transport: fakeTransport,
logger: null
logger: null,
} as IHttpConnectionOptions;
@ -196,17 +178,16 @@ describe("Connection", () => {
let negotiateUrl: string;
let connection: HttpConnection;
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
negotiateUrl = url;
httpClient: new TestHttpClient()
.on("POST", r => {
negotiateUrl = r.url;
connection.stop();
return Promise.resolve("{}");
},
get(url: string): Promise<string> {
return "{}";
})
.on("GET", r => {
connection.stop();
return Promise.resolve("");
}
},
return "";
}),
logger: null
} as IHttpConnectionOptions;
@ -231,14 +212,9 @@ describe("Connection", () => {
}
it(`cannot be started if requested ${TransportType[requestedTransport]} transport not available on server`, async done => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
httpClient: new TestHttpClient()
.on("POST", r => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("GET", r => ""),
transport: requestedTransport,
logger: null
} as IHttpConnectionOptions;
@ -258,14 +234,9 @@ describe("Connection", () => {
it("cannot be started if no transport available on server and no transport requested", async done => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
httpClient: new TestHttpClient()
.on("POST", r => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("GET", r => ""),
logger: null
} as IHttpConnectionOptions;
@ -283,14 +254,7 @@ describe("Connection", () => {
it('does not send negotiate request if WebSockets transport requested explicitly', async done => {
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.reject("Should not be called");
},
get(url: string): Promise<string> {
return Promise.reject("Should not be called");
}
},
httpClient: new TestHttpClient(),
transport: TransportType.WebSockets,
logger: null
} as IHttpConnectionOptions;
@ -327,14 +291,9 @@ describe("Connection", () => {
} as ITransport;
let options: IHttpConnectionOptions = {
httpClient: <IHttpClient>{
post(url: string): Promise<string> {
return Promise.resolve("{ \"connectionId\": \"42\", \"availableTransports\": [] }");
},
get(url: string): Promise<string> {
return Promise.resolve("");
}
},
httpClient: new TestHttpClient()
.on("POST", r => "{ \"connectionId\": \"42\", \"availableTransports\": [] }")
.on("GET", r => ""),
transport: fakeTransport,
logger: null
} as IHttpConnectionOptions;

View File

@ -0,0 +1,90 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpClient, HttpRequest, HttpResponse } from "../Microsoft.AspNetCore.SignalR.Client.TS/HttpClient"
type TestHttpHandlerResult = HttpResponse | string;
export type TestHttpHandler = (request: HttpRequest, next?: (request: HttpRequest) => Promise<HttpResponse>) => Promise<TestHttpHandlerResult> | TestHttpHandlerResult;
export class TestHttpClient extends HttpClient {
private handler: (request: HttpRequest) => Promise<HttpResponse>;
constructor() {
super();
this.handler = (request: HttpRequest) =>
Promise.reject(`Request has no handler: ${request.method} ${request.url}`);
}
send(request: HttpRequest): Promise<HttpResponse> {
return this.handler(request);
}
on(handler: TestHttpHandler): TestHttpClient;
on(method: string | RegExp, handler: TestHttpHandler): TestHttpClient;
on(method: string | RegExp, url: string, handler: TestHttpHandler): TestHttpClient;
on(method: string | RegExp, url: RegExp, handler: TestHttpHandler): TestHttpClient;
on(methodOrHandler: string | RegExp | TestHttpHandler, urlOrHandler?: string | RegExp | TestHttpHandler, handler?: TestHttpHandler): TestHttpClient {
let method: string | RegExp;
let url: string | RegExp;
if ((typeof methodOrHandler === "string") || (methodOrHandler instanceof RegExp)) {
method = methodOrHandler;
}
else if (methodOrHandler) {
handler = methodOrHandler;
}
if ((typeof urlOrHandler === "string") || (urlOrHandler instanceof RegExp)) {
url = urlOrHandler;
}
else if (urlOrHandler) {
handler = urlOrHandler;
}
// TypeScript callers won't be able to do this, because TypeScript checks this for us.
if (!handler) {
throw new Error("Missing required argument: 'handler'");
}
let oldHandler = this.handler;
let newHandler = async (request: HttpRequest) => {
if (matches(method, request.method) && matches(url, request.url)) {
let promise = handler(request, oldHandler);
let val: TestHttpHandlerResult;
if (promise instanceof Promise) {
val = await promise;
} else {
val = promise;
}
if (typeof val === "string") {
return new HttpResponse(200, "OK", val);
}
else {
return val;
}
}
else {
return await oldHandler(request);
}
};
this.handler = newHandler;
return this;
}
}
function matches(pattern: string | RegExp, actual: string): boolean {
// Null or undefined pattern matches all.
if (!pattern) {
return true;
}
if (typeof pattern === "string") {
return actual === pattern;
}
else {
return pattern.test(actual);
}
}

View File

@ -3,16 +3,20 @@
import { clearTimeout, setTimeout } from "timers";
export function asyncit(expectation: string, assertion?: () => Promise<any>, timeout?: number): void {
export function asyncit(expectation: string, assertion?: () => Promise<any> | void, timeout?: number): void {
let testFunction: (done: DoneFn) => void;
if (assertion) {
testFunction = done => {
assertion()
.then(() => done())
.catch((err) => {
fail(err);
done();
});
let promise = assertion();
if (promise) {
promise.then(() => done())
.catch((err) => {
fail(err);
done();
});
} else {
done();
}
};
}
@ -54,4 +58,4 @@ export class PromiseSource<T> {
reject(reason?: any) {
this.rejecter(reason);
}
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Rough polyfill of https://developer.mozilla.org/en-US/docs/Web/API/AbortController
// We don't actually ever use the API being polyfilled, we always use the polyfill because
// it's a very new API right now.
export class AbortController implements AbortSignal {
private isAborted: boolean = false;
public onabort: () => void;
abort() {
if (!this.isAborted) {
this.isAborted = true;
if (this.onabort) {
this.onabort();
}
}
}
get signal(): AbortSignal {
return this;
}
get aborted(): boolean {
return this.isAborted;
}
}
export interface AbortSignal {
aborted: boolean;
onabort: () => void;
}

View File

@ -7,4 +7,10 @@ export class HttpError extends Error {
super(errorMessage);
this.statusCode = statusCode;
}
}
}
export class TimeoutError extends Error {
constructor(errorMessage: string = "A timeout occurred.") {
super(errorMessage);
}
}

View File

@ -1,36 +1,86 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { HttpError } from "./HttpError"
import { TimeoutError, HttpError } from "./Errors";
import { AbortSignal } from "./AbortController";
export interface IHttpClient {
get(url: string, headers?: Map<string, string>): Promise<string>;
post(url: string, content: string, headers?: Map<string, string>): Promise<string>;
export interface HttpRequest {
method?: string,
url?: string,
content?: string | ArrayBuffer,
headers?: Map<string, string>,
responseType?: XMLHttpRequestResponseType,
abortSignal?: AbortSignal,
timeout?: number,
}
export class HttpClient implements IHttpClient {
get(url: string, headers?: Map<string, string>): Promise<string> {
return this.xhr("GET", url, headers);
export class HttpResponse {
constructor(statusCode: number, statusText: string, content: string);
constructor(statusCode: number, statusText: string, content: ArrayBuffer);
constructor(
public readonly statusCode: number,
public readonly statusText: string,
public readonly content: string | ArrayBuffer) {
}
}
export abstract class HttpClient {
get(url: string): Promise<HttpResponse>;
get(url: string, options: HttpRequest): Promise<HttpResponse>;
get(url: string, options?: HttpRequest): Promise<HttpResponse> {
return this.send({
...options,
method: "GET",
url: url,
});
}
post(url: string, content: string, headers?: Map<string, string>): Promise<string> {
return this.xhr("POST", url, headers, content);
post(url: string): Promise<HttpResponse>;
post(url: string, options: HttpRequest): Promise<HttpResponse>;
post(url: string, options?: HttpRequest): Promise<HttpResponse> {
return this.send({
...options,
method: "POST",
url: url,
});
}
private xhr(method: string, url: string, headers?: Map<string, string>, content?: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
abstract send(request: HttpRequest): Promise<HttpResponse>;
}
export class DefaultHttpClient extends HttpClient {
send(request: HttpRequest): Promise<HttpResponse> {
return new Promise<HttpResponse>((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.open(request.method, request.url, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
if (headers) {
headers.forEach((value, header) => xhr.setRequestHeader(header, value));
if (request.headers) {
request.headers.forEach((value, header) => xhr.setRequestHeader(header, value));
}
if (request.responseType) {
xhr.responseType = request.responseType;
}
if (request.abortSignal) {
request.abortSignal.onabort = () => {
xhr.abort();
};
}
if (request.timeout) {
xhr.timeout = request.timeout;
}
xhr.send(content);
xhr.onload = () => {
if (request.abortSignal) {
request.abortSignal.onabort = null;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response || xhr.responseText);
resolve(new HttpResponse(xhr.status, xhr.statusText, xhr.response || xhr.responseText))
}
else {
reject(new HttpError(xhr.statusText, xhr.status));
@ -40,6 +90,12 @@ export class HttpClient implements IHttpClient {
xhr.onerror = () => {
reject(new HttpError(xhr.statusText, xhr.status));
}
xhr.ontimeout = () => {
reject(new TimeoutError());
}
xhr.send(request.content || "");
});
}
}

View File

@ -4,7 +4,7 @@
import { DataReceived, ConnectionClosed } from "./Common"
import { IConnection } from "./IConnection"
import { ITransport, TransferMode, TransportType, WebSocketTransport, ServerSentEventsTransport, LongPollingTransport } from "./Transports"
import { IHttpClient, HttpClient } from "./HttpClient"
import { HttpClient, DefaultHttpClient } from "./HttpClient"
import { IHttpConnectionOptions } from "./IHttpConnectionOptions"
import { ILogger, LogLevel } from "./ILogger"
import { LoggerFactory } from "./Loggers"
@ -24,7 +24,7 @@ export class HttpConnection implements IConnection {
private connectionState: ConnectionState;
private baseUrl: string;
private url: string;
private readonly httpClient: IHttpClient;
private readonly httpClient: HttpClient;
private readonly logger: ILogger;
private readonly options: IHttpConnectionOptions;
private transport: ITransport;
@ -37,7 +37,7 @@ export class HttpConnection implements IConnection {
this.logger = LoggerFactory.createLogger(options.logger);
this.baseUrl = this.resolveUrl(url);
options = options || {};
this.httpClient = options.httpClient || new HttpClient();
this.httpClient = options.httpClient || new DefaultHttpClient();
this.connectionState = ConnectionState.Disconnected;
this.options = options;
}
@ -67,9 +67,12 @@ export class HttpConnection implements IConnection {
headers.set("Authorization", `Bearer ${this.options.accessToken()}`);
}
let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), "", headers);
let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.baseUrl), {
content: "",
headers
});
let negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload);
let negotiateResponse: INegotiateResponse = JSON.parse(<string>negotiatePayload.content);
this.connectionId = negotiateResponse.connectionId;
// the user tries to stop the the connection when it is being started

View File

@ -1,12 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { IHttpClient } from "./HttpClient"
import { HttpClient } from "./HttpClient"
import { TransportType, ITransport } from "./Transports"
import { ILogger, LogLevel } from "./ILogger";
export interface IHttpConnectionOptions {
httpClient?: IHttpClient;
httpClient?: HttpClient;
transport?: TransportType | ITransport;
logger?: ILogger | LogLevel;
accessToken?: () => string;

View File

@ -1,11 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
import { DataReceived, TransportClosed } from "./Common"
import { IHttpClient } from "./HttpClient"
import { HttpError } from "./HttpError"
import { ILogger, LogLevel } from "./ILogger"
import { IConnection } from "./IConnection"
import { DataReceived, TransportClosed } from "./Common";
import { HttpClient, HttpRequest } from "./HttpClient";
import { HttpError, TimeoutError } from "./Errors";
import { ILogger, LogLevel } from "./ILogger";
import { IConnection } from "./IConnection";
import { AbortController } from "./AbortController";
export enum TransportType {
WebSockets,
@ -103,13 +104,13 @@ export class WebSocketTransport implements ITransport {
}
export class ServerSentEventsTransport implements ITransport {
private readonly httpClient: IHttpClient;
private readonly httpClient: HttpClient;
private readonly accessToken: () => string;
private readonly logger: ILogger;
private eventSource: EventSource;
private url: string;
constructor(httpClient: IHttpClient, accessToken: () => string, logger: ILogger) {
constructor(httpClient: HttpClient, accessToken: () => string, logger: ILogger) {
this.httpClient = httpClient;
this.accessToken = accessToken;
this.logger = logger;
@ -183,23 +184,23 @@ export class ServerSentEventsTransport implements ITransport {
}
export class LongPollingTransport implements ITransport {
private readonly httpClient: IHttpClient;
private readonly httpClient: HttpClient;
private readonly accessToken: () => string;
private readonly logger: ILogger;
private url: string;
private pollXhr: XMLHttpRequest;
private shouldPoll: boolean;
private pollAbort: AbortController;
constructor(httpClient: IHttpClient, accessToken: () => string, logger: ILogger) {
constructor(httpClient: HttpClient, accessToken: () => string, logger: ILogger) {
this.httpClient = httpClient;
this.accessToken = accessToken;
this.logger = logger;
this.pollAbort = new AbortController();
}
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
this.url = url;
this.shouldPoll = true;
// Set a flag indicating we have inherent keep-alive in this transport.
connection.features.inherentKeepAlive = true;
@ -213,73 +214,70 @@ export class LongPollingTransport implements ITransport {
return Promise.resolve(requestedTransferMode);
}
private poll(url: string, transferMode: TransferMode): void {
if (!this.shouldPoll) {
return;
private async poll(url: string, transferMode: TransferMode): Promise<void> {
let pollOptions: HttpRequest = {
timeout: 120000,
abortSignal: this.pollAbort.signal,
headers: new Map<string, string>(),
};
if (transferMode === TransferMode.Binary) {
pollOptions.responseType = "arraybuffer";
}
let pollXhr = new XMLHttpRequest();
if (this.accessToken) {
pollOptions.headers.set("Authorization", `Bearer ${this.accessToken()}`);
}
pollXhr.onload = () => {
if (pollXhr.status == 200) {
if (this.onreceive) {
try {
let response = transferMode === TransferMode.Text
? pollXhr.responseText
: pollXhr.response;
while (!this.pollAbort.signal.aborted) {
try {
let pollUrl = `${url}&_=${Date.now()}`;
this.logger.log(LogLevel.Trace, `(LongPolling transport) polling: ${pollUrl}`);
let response = await this.httpClient.get(pollUrl, pollOptions)
if (response.statusCode === 204) {
this.logger.log(LogLevel.Information, "(LongPolling transport) Poll terminated by server");
if (response) {
this.logger.log(LogLevel.Trace, `(LongPolling transport) data received: ${response}`);
this.onreceive(response);
// Poll terminated by server
if (this.onclose) {
this.onclose();
}
this.pollAbort.abort();
}
else if (response.statusCode !== 200) {
this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`);
// Unexpected status code
if (this.onclose) {
this.onclose(new HttpError(response.statusText, response.statusCode));
}
this.pollAbort.abort();
}
else {
// Process the response
if (response.content) {
this.logger.log(LogLevel.Trace, `(LongPolling transport) data received: ${response.content}`);
if (this.onreceive) {
this.onreceive(response.content);
}
else {
this.logger.log(LogLevel.Information, "(LongPolling transport) timed out");
}
} catch (error) {
if (this.onclose) {
this.onclose(error);
}
return;
}
else {
// This is another way timeout manifest.
this.logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
}
}
this.poll(url, transferMode);
}
else if (this.pollXhr.status == 204) {
if (this.onclose) {
this.onclose();
} catch (e) {
if (e instanceof TimeoutError) {
// Ignore timeouts and reissue the poll.
this.logger.log(LogLevel.Trace, "(LongPolling transport) Poll timed out, reissuing.");
} else {
// Close the connection with the error as the result.
if (this.onclose) {
this.onclose(e);
}
this.pollAbort.abort();
}
}
else {
if (this.onclose) {
this.onclose(new HttpError(pollXhr.statusText, pollXhr.status));
}
}
};
pollXhr.onerror = () => {
if (this.onclose) {
// network related error or denied cross domain request
this.onclose(new Error("Sending HTTP request failed."));
}
};
pollXhr.ontimeout = () => {
this.poll(url, transferMode);
}
this.pollXhr = pollXhr;
this.pollXhr.open("GET", `${url}&_=${Date.now()}`, true);
if (this.accessToken) {
this.pollXhr.setRequestHeader("Authorization", `Bearer ${this.accessToken()}`);
}
if (transferMode === TransferMode.Binary) {
this.pollXhr.responseType = "arraybuffer";
}
// TODO: consider making timeout configurable
this.pollXhr.timeout = 120000;
this.pollXhr.send();
}
async send(data: any): Promise<void> {
@ -287,11 +285,7 @@ export class LongPollingTransport implements ITransport {
}
stop(): Promise<void> {
this.shouldPoll = false;
if (this.pollXhr) {
this.pollXhr.abort();
this.pollXhr = null;
}
this.pollAbort.abort();
return Promise.resolve();
}
@ -299,12 +293,15 @@ export class LongPollingTransport implements ITransport {
onclose: TransportClosed;
}
async function send(httpClient: IHttpClient, url: string, accessToken: () => string, data: any): Promise<void> {
async function send(httpClient: HttpClient, url: string, accessToken: () => string, content: string | ArrayBuffer): Promise<void> {
let headers;
if (accessToken) {
headers = new Map<string, string>();
headers.set("Authorization", `Bearer ${accessToken()}`)
}
await httpClient.post(url, data, headers);
await httpClient.post(url, {
content,
headers
});
}

View File

@ -1,9 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1 id="head1"></h1>
<div>
@ -48,16 +50,15 @@
<ul id="message-list"></ul>
</body>
</html>
<script type="text/javascript">
if (typeof Promise === 'undefined')
{
if (typeof Promise === 'undefined') {
document.write(
'<script type="text/javascript" src="lib/signalr-client/signalr-clientES5.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr-client/signalr-msgpackprotocolES5.js"><\/script>');
}
else
{
else {
document.write(
'<script type="text/javascript" src="lib/signalr-client/signalr-client.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr-client/signalr-msgpackprotocol.js"><\/script>');
@ -65,102 +66,102 @@
</script>
<script src="utils.js"></script>
<script>
var isConnected = false;
function invoke(connection, method) {
if (!isConnected) {
return;
}
var argsArray = Array.prototype.slice.call(arguments);
connection.invoke.apply(connection, argsArray.slice(1))
.then(function(result) {
var isConnected = false;
function invoke(connection, method) {
if (!isConnected) {
return;
}
var argsArray = Array.prototype.slice.call(arguments);
connection.invoke.apply(connection, argsArray.slice(1))
.then(function (result) {
console.log("invocation completed successfully: " + (result === null ? '(null)' : result));
if (result) {
addLine('message-list', result);
}
})
.catch(function(err) {
.catch(function (err) {
addLine('message-list', err, 'red');
});
}
}
function getText(id) {
return document.getElementById(id).value;
}
function getText(id) {
return document.getElementById(id).value;
}
let transportType = signalR.TransportType[getParameterByName('transport')] || signalR.TransportType.WebSockets;
let logger = new signalR.ConsoleLogger(signalR.LogLevel.Information);
let hubRoute = getParameterByName('hubType') || "default";
console.log('Hub Route:' + hubRoute);
let transportType = signalR.TransportType[getParameterByName('transport')] || signalR.TransportType.WebSockets;
let logger = new signalR.ConsoleLogger(signalR.LogLevel.Trace);
let hubRoute = getParameterByName('hubType') || "default";
console.log('Hub Route:' + hubRoute);
document.getElementById('head1').innerHTML = signalR.TransportType[transportType];
document.getElementById('head1').innerHTML = signalR.TransportType[transportType];
let connectButton = document.getElementById('connect');
let disconnectButton = document.getElementById('disconnect');
disconnectButton.disabled = true;
var connection;
click('connect', function(event) {
connectButton.disabled = true;
disconnectButton.disabled = false;
console.log('http://' + document.location.host + '/' + hubRoute);
connection = new signalR.HubConnection(hubRoute, { transport: transportType, logging: logger });
connection.on('Send', function(msg) {
addLine('message-list', msg);
});
connection.onclose(function(e) {
if (e) {
addLine('message-list', 'Connection closed with error: ' + e, 'red');
}
else {
addLine('message-list', 'Disconnected', 'green');
}
});
connection.start()
.then(function() {
isConnected = true;
addLine('message-list', 'Connected successfully', 'green');
})
.catch(function(err) {
addLine('message-list', err, 'red');
});
});
click('disconnect', function(event) {
connectButton.disabled = false;
let connectButton = document.getElementById('connect');
let disconnectButton = document.getElementById('disconnect');
disconnectButton.disabled = true;
connection.stop()
.then(function() {
isConnected = false;
var connection;
click('connect', function (event) {
connectButton.disabled = true;
disconnectButton.disabled = false;
console.log('http://' + document.location.host + '/' + hubRoute);
connection = new signalR.HubConnection(hubRoute, { transport: transportType, logging: logger });
connection.on('Send', function (msg) {
addLine('message-list', msg);
});
});
click('broadcast', function(event) {
let data = getText('message-text');
invoke(connection, 'Send', data);
});
connection.onclose(function (e) {
if (e) {
addLine('message-list', 'Connection closed with error: ' + e, 'red');
}
else {
addLine('message-list', 'Disconnected', 'green');
}
});
click('join-group', function(event) {
let groupName = getText('message-text');
invoke(connection, 'JoinGroup', groupName);
});
connection.start()
.then(function () {
isConnected = true;
addLine('message-list', 'Connected successfully', 'green');
})
.catch(function (err) {
addLine('message-list', err, 'red');
});
});
click('leave-group', function(event) {
let groupName = getText('message-text');
invoke(connection, 'LeaveGroup', groupName);
});
click('disconnect', function (event) {
connectButton.disabled = false;
disconnectButton.disabled = true;
connection.stop()
.then(function () {
isConnected = false;
});
});
click('groupmsg', function(event) {
let groupName = getText('target');
let message = getText('private-message-text');
invoke(connection, 'SendToGroup', groupName, message);
});
click('broadcast', function (event) {
let data = getText('message-text');
invoke(connection, 'Send', data);
});
click('send', function(event) {
let data = getText('me-message-text');
invoke(connection, 'Echo', data);
});
click('join-group', function (event) {
let groupName = getText('message-text');
invoke(connection, 'JoinGroup', groupName);
});
click('leave-group', function (event) {
let groupName = getText('message-text');
invoke(connection, 'LeaveGroup', groupName);
});
click('groupmsg', function (event) {
let groupName = getText('target');
let message = getText('private-message-text');
invoke(connection, 'SendToGroup', groupName, message);
});
click('send', function (event) {
let data = getText('me-message-text');
invoke(connection, 'Echo', data);
});
</script>

View File

@ -1,9 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1 id="transportName">Unknown Transport</h1>
@ -14,43 +16,42 @@
<ul id="messages"></ul>
<script type="text/javascript">
if (typeof Promise === 'undefined')
{
if (typeof Promise === 'undefined') {
document.write('<script type="text/javascript" src="lib/signalr-client/signalr-clientES5.js"><\/script>');
}
else
{
else {
document.write('<script type="text/javascript" src="lib/signalr-client/signalr-client.js"><\/script>');
}
</script>
<script src="utils.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
let transportType = signalR.TransportType[getParameterByName('transport')] || signalR.TransportType.WebSockets;
document.getElementById('transportName').innerHTML = signalR.TransportType[transportType];
let url = 'http://' + document.location.host + '/chat';
let connection = new signalR.HttpConnection(url, { transport: transportType, logging: new signalR.ConsoleLogger(signalR.LogLevel.Information) });
let connection = new signalR.HttpConnection(url, { transport: transportType, logging: new signalR.ConsoleLogger(signalR.LogLevel.Trace) });
connection.onreceive = function(data) {
connection.onreceive = function (data) {
let child = document.createElement('li');
child.innerText = data;
document.getElementById('messages').appendChild(child);
};
document.getElementById('sendmessage').addEventListener('submit', function(event) {
document.getElementById('sendmessage').addEventListener('submit', function (event) {
let data = document.getElementById('data').value;
connection.send(data);
event.preventDefault();
});
connection.start().then(function() {
connection.start().then(function () {
console.log("Opened");
}, function() {
}, function () {
console.log("Error opening connection");
});
});
</script>
</body>
</html>

View File

@ -1,9 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<h1 id="transportName">Unknown Transport</h1>
@ -24,14 +26,12 @@
<ul id="messages"></ul>
<script type="text/javascript">
if (typeof Promise === 'undefined')
{
if (typeof Promise === 'undefined') {
document.write(
'<script type="text/javascript" src="lib/signalr-client/signalr-clientES5.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr-client/signalr-msgpackprotocolES5.js"><\/script>');
}
else
{
else {
document.write(
'<script type="text/javascript" src="lib/signalr-client/signalr-client.js"><\/script>' +
'<script type="text/javascript" src="lib/signalr-client/signalr-msgpackprotocol.js"><\/script>');
@ -39,7 +39,7 @@
</script>
<script src="utils.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
let resultsList = document.getElementById('resultsList');
let channelButton = document.getElementById('channelButton');
let observableButton = document.getElementById('observableButton');
@ -48,7 +48,7 @@
let connectButton = document.getElementById('connectButton');
let disconnectButton = document.getElementById('disconnectButton');
let logger = new signalR.ConsoleLogger(signalR.LogLevel.Information);
let logger = new signalR.ConsoleLogger(signalR.LogLevel.Trace);
let transportType = signalR.TransportType[getParameterByName('transport')] || signalR.TransportType.WebSockets;
let invocationCounter = 0;
@ -107,11 +107,12 @@
addLine('resultsList', method + '(' + id + '):' + err, 'red');
},
complete: function () {
addLine('resultsList', method + '(' + id + '): complete', 'green');
addLine('resultsList', method + '(' + id + '): complete', 'green');
}
});
}
});
</script>
</body>
</html>