Add WebSocket and EventSource polyfills to options (#2408)
This commit is contained in:
parent
4f85ca2b1d
commit
10efae64e0
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]}'`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue