Add WebSocket and EventSource polyfills to options (#2408)

This commit is contained in:
BrennanConroy 2018-06-01 09:25:25 -07:00 committed by GitHub
parent 4f85ca2b1d
commit 10efae64e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 18 deletions

View File

@ -2,7 +2,7 @@
// 'node' indicates to Jenkins that the enclosed block runs on a node that matches
// the label 'windows-with-vs'
simpleNode('Windows.10.Enterprise.RS3.ASPNET') {
simpleNode('Windows.10.Amd64.EnterpriseRS3.ASPNET.Open') {
stage ('Checking out source') {
checkout scm
}

View File

@ -20,7 +20,6 @@ The following polyfills are required to use the client in Node.js applications:
- `XmlHttpRequest` - always
- `WebSockets` - to use the WebSockets transport
- `EventSource` - to use the ServerSentEvents transport
- `btoa/atob` - to use binary protocols (e.g. MessagePack) over text transports (ServerSentEvents)
### Example (Browser)

View File

@ -7,6 +7,7 @@ import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
import { ILogger, LogLevel } from "./ILogger";
import { HttpTransportType, ITransport, TransferFormat } from "./ITransport";
import { LongPollingTransport } from "./LongPollingTransport";
import { EventSourceConstructor, WebSocketConstructor } from "./Polyfills";
import { ServerSentEventsTransport } from "./ServerSentEventsTransport";
import { Arg, createLogger } from "./Utils";
import { WebSocketTransport } from "./WebSocketTransport";
@ -55,6 +56,13 @@ export class HttpConnection implements IConnection {
options = options || {};
options.logMessageContent = options.logMessageContent || false;
if (typeof WebSocket !== "undefined" && !options.WebSocket) {
options.WebSocket = WebSocket;
}
if (typeof EventSource !== "undefined" && !options.EventSource) {
options.EventSource = EventSource;
}
this.httpClient = options.httpClient || new DefaultHttpClient(this.logger);
this.connectionState = ConnectionState.Disconnected;
this.options = options;
@ -253,9 +261,15 @@ export class HttpConnection implements IConnection {
private constructTransport(transport: HttpTransportType) {
switch (transport) {
case HttpTransportType.WebSockets:
return new WebSocketTransport(this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
if (!this.options.WebSocket) {
throw new Error("'WebSocket' is not supported in your environment.");
}
return new WebSocketTransport(this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.WebSocket);
case HttpTransportType.ServerSentEvents:
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
if (!this.options.EventSource) {
throw new Error("'EventSource' is not supported in your environment.");
}
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false, this.options.EventSource);
case HttpTransportType.LongPolling:
return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
default:
@ -271,8 +285,8 @@ export class HttpConnection implements IConnection {
const transferFormats = endpoint.transferFormats.map((s) => TransferFormat[s]);
if (transportMatches(requestedTransport, transport)) {
if (transferFormats.indexOf(requestedTransferFormat) >= 0) {
if ((transport === HttpTransportType.WebSockets && typeof WebSocket === "undefined") ||
(transport === HttpTransportType.ServerSentEvents && typeof EventSource === "undefined")) {
if ((transport === HttpTransportType.WebSockets && !this.options.WebSocket) ||
(transport === HttpTransportType.ServerSentEvents && !this.options.EventSource)) {
this.logger.log(LogLevel.Debug, `Skipping transport '${HttpTransportType[transport]}' because it is not supported in your environment.'`);
} else {
this.logger.log(LogLevel.Debug, `Selecting transport '${HttpTransportType[transport]}'`);

View File

@ -4,6 +4,7 @@
import { HttpClient } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { HttpTransportType, ITransport } from "./ITransport";
import { EventSourceConstructor, WebSocketConstructor } from "./Polyfills";
/** Options provided to the 'withUrl' method on {@link HubConnectionBuilder} to configure options for the HTTP-based transports. */
export interface IHttpConnectionOptions {
@ -38,4 +39,18 @@ export interface IHttpConnectionOptions {
* Negotiation can only be skipped when the {@link transport} property is set to 'HttpTransportType.WebSockets'.
*/
skipNegotiation?: boolean;
// Used for unit testing and code spelunkers
/** A constructor that can be used to create a WebSocket.
*
* @internal
*/
WebSocket?: WebSocketConstructor;
// Used for unit testing and code spelunkers
/** A constructor that can be used to create an EventSource.
*
* @internal
*/
EventSource?: EventSourceConstructor;
}

View File

@ -0,0 +1,16 @@
// 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.
// Not exported from index
export interface EventSourceConstructor {
new(url: string, eventSourceInitDict?: EventSourceInit): EventSource;
}
export interface WebSocketConstructor {
new(url: string, protocols?: string | string[]): WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
}

View File

@ -4,6 +4,7 @@
import { HttpClient } from "./HttpClient";
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { EventSourceConstructor } from "./Polyfills";
import { Arg, getDataDetail, sendMessage } from "./Utils";
export class ServerSentEventsTransport implements ITransport {
@ -11,17 +12,20 @@ export class ServerSentEventsTransport implements ITransport {
private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
private readonly logger: ILogger;
private readonly logMessageContent: boolean;
private readonly eventSourceConstructor: EventSourceConstructor;
private eventSource?: EventSource;
private url?: string;
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean) {
constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, eventSourceConstructor: EventSourceConstructor) {
this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory;
this.logger = logger;
this.logMessageContent = logMessageContent;
this.eventSourceConstructor = eventSourceConstructor;
this.onreceive = null;
this.onclose = null;
@ -32,10 +36,6 @@ export class ServerSentEventsTransport implements ITransport {
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
if (typeof (EventSource) === "undefined") {
throw new Error("'EventSource' is not supported in your environment.");
}
this.logger.log(LogLevel.Trace, "(SSE transport) Connecting");
if (this.accessTokenFactory) {
@ -52,7 +52,7 @@ export class ServerSentEventsTransport implements ITransport {
reject(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"));
}
const eventSource = new EventSource(url, { withCredentials: true });
const eventSource = new this.eventSourceConstructor(url, { withCredentials: true });
try {
eventSource.onmessage = (e: MessageEvent) => {

View File

@ -3,21 +3,25 @@
import { ILogger, LogLevel } from "./ILogger";
import { ITransport, TransferFormat } from "./ITransport";
import { WebSocketConstructor } from "./Polyfills";
import { Arg, getDataDetail } from "./Utils";
export class WebSocketTransport implements ITransport {
private readonly logger: ILogger;
private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
private readonly logMessageContent: boolean;
private readonly webSocketConstructor: WebSocketConstructor;
private webSocket?: WebSocket;
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
constructor(accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean) {
constructor(accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger,
logMessageContent: boolean, webSocketConstructor: WebSocketConstructor) {
this.logger = logger;
this.accessTokenFactory = accessTokenFactory;
this.logMessageContent = logMessageContent;
this.webSocketConstructor = webSocketConstructor;
this.onreceive = null;
this.onclose = null;
@ -28,10 +32,6 @@ export class WebSocketTransport implements ITransport {
Arg.isRequired(transferFormat, "transferFormat");
Arg.isIn(transferFormat, TransferFormat, "transferFormat");
if (typeof (WebSocket) === "undefined") {
throw new Error("'WebSocket' is not supported in your environment.");
}
this.logger.log(LogLevel.Trace, "(WebSockets transport) Connecting");
if (this.accessTokenFactory) {
@ -43,7 +43,7 @@ export class WebSocketTransport implements ITransport {
return new Promise<void>((resolve, reject) => {
url = url.replace(/^http/, "ws");
const webSocket = new WebSocket(url);
const webSocket = new this.webSocketConstructor(url);
if (transferFormat === TransferFormat.Binary) {
webSocket.binaryType = "arraybuffer";
}

View File

@ -8,6 +8,9 @@ import { HttpTransportType, ITransport, TransferFormat } from "../src/ITransport
import { HttpError } from "../src/Errors";
import { LogLevel } from "../src/ILogger";
import { EventSourceConstructor, WebSocketConstructor } from "../src/Polyfills";
import { TextMessageFormat } from "../src/TextMessageFormat";
import { WebSocketTransport } from "../src/WebSocketTransport";
import { eachEndpointUrl, eachTransport } from "./Common";
import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource } from "./Utils";
@ -577,6 +580,119 @@ describe("HttpConnection", () => {
// Force TypeScript to let us call the constructor incorrectly :)
expect(() => new (HttpConnection as any)()).toThrowError("The 'url' argument is required.");
});
it("uses global WebSocket if defined", async () => {
// tslint:disable-next-line:no-string-literal
global["WebSocket"] = class WebSocket {
constructor(url: string, protocols?: string | string[]) {
throw new Error("WebSocket constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start())
.rejects
.toThrow("WebSocket constructor called.");
// tslint:disable-next-line:no-string-literal
delete global["WebSocket"];
});
it("uses global EventSource if defined", async () => {
let eventSourceConstructorCalled: boolean = false;
// tslint:disable-next-line:no-string-literal
global["EventSource"] = class EventSource {
constructor(url: string, eventSourceInitDict?: EventSourceInit) {
eventSourceConstructorCalled = true;
throw new Error("EventSource constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient().on("POST", (r) => {
return {
availableTransports: [
{ transport: "ServerSentEvents", transferFormats: ["Text"] },
],
connectionId: defaultConnectionId,
};
}),
transport: HttpTransportType.ServerSentEvents,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
expect(eventSourceConstructorCalled).toEqual(true);
// tslint:disable-next-line:no-string-literal
delete global["EventSource"];
});
it("uses EventSource constructor from options if provided", async () => {
let eventSourceConstructorCalled: boolean = false;
const customEventSourceType = class EventSource {
constructor(url: string, eventSourceInitDict?: EventSourceInit) {
eventSourceConstructorCalled = true;
throw new Error("EventSource constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
EventSource: customEventSourceType as EventSourceConstructor,
httpClient: new TestHttpClient().on("POST", (r) => {
return {
availableTransports: [
{ transport: "ServerSentEvents", transferFormats: ["Text"] },
],
connectionId: defaultConnectionId,
};
}),
transport: HttpTransportType.ServerSentEvents,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start(TransferFormat.Text))
.rejects
.toThrow("Unable to initialize any of the available transports.");
expect(eventSourceConstructorCalled).toEqual(true);
});
it("uses WebSocket constructor from options if provided", async () => {
const customWebSocketType = class WebSocket {
constructor(url: string, protocols?: string | string[]) {
throw new Error("WebSocket constructor called.");
}
};
const options: IHttpConnectionOptions = {
...commonOptions,
WebSocket: customWebSocketType as WebSocketConstructor,
skipNegotiation: true,
transport: HttpTransportType.WebSockets,
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
await expect(connection.start())
.rejects
.toThrow("WebSocket constructor called.");
});
});
describe("startAsync", () => {