TypeScript strict mode (#2388)

This commit is contained in:
James Newton-King 2018-05-31 12:04:48 +12:00 committed by GitHub
parent 8d680db112
commit 30a59f6df7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 313 additions and 233 deletions

View File

@ -350,7 +350,7 @@ describe("hubConnection", () => {
.build(); .build();
hubConnection.onclose((error) => { hubConnection.onclose((error) => {
expect(error.message).toEqual("Server returned an error on close: Connection closed with an error. InvalidOperationException: Unable to resolve service for type 'System.Object' while attempting to activate 'FunctionalTests.UncreatableHub'."); expect(error!.message).toEqual("Server returned an error on close: Connection closed with an error. InvalidOperationException: Unable to resolve service for type 'System.Object' while attempting to activate 'FunctionalTests.UncreatableHub'.");
done(); done();
}); });
hubConnection.start(); hubConnection.start();
@ -632,7 +632,12 @@ describe("hubConnection", () => {
const defaultClient = new DefaultHttpClient(TestLogger.instance); const defaultClient = new DefaultHttpClient(TestLogger.instance);
class TestClient extends HttpClient { class TestClient extends HttpClient {
public pollPromise: Promise<HttpResponse>; public pollPromise: Promise<HttpResponse> | null;
constructor() {
super();
this.pollPromise = null;
}
public send(request: HttpRequest): Promise<HttpResponse> { public send(request: HttpRequest): Promise<HttpResponse> {
if (request.method === "GET") { if (request.method === "GET") {

View File

@ -56,7 +56,7 @@ class WebDriverReporter implements jasmine.CustomReporter {
// Just report the first failure // Just report the first failure
this.taplog(" ---"); this.taplog(" ---");
if (result.failedExpectations.length > 0) { if (result.failedExpectations && result.failedExpectations.length > 0) {
this.taplog(" - messages:"); this.taplog(" - messages:");
for (const expectation of result.failedExpectations) { for (const expectation of result.failedExpectations) {
// Include YAML block with failed expectations // Include YAML block with failed expectations

View File

@ -37,7 +37,19 @@ export class MessagePackHubProtocol implements IHubProtocol {
if (logger === null) { if (logger === null) {
logger = NullLogger.instance; logger = NullLogger.instance;
} }
return BinaryMessageFormat.parse(input).map((m) => this.parseMessage(m, logger));
const messages = BinaryMessageFormat.parse(input);
const hubMessages = [];
for (const message of messages) {
const parsedMessage = this.parseMessage(message, logger);
// Can be null for an unknown message. Unknown message is logged in parseMessage
if (parsedMessage) {
hubMessages.push(parsedMessage);
}
}
return hubMessages;
} }
/** Writes the specified HubMessage to an ArrayBuffer and returns it. /** Writes the specified HubMessage to an ArrayBuffer and returns it.
@ -61,7 +73,7 @@ export class MessagePackHubProtocol implements IHubProtocol {
} }
} }
private parseMessage(input: Uint8Array, logger: ILogger): HubMessage { private parseMessage(input: Uint8Array, logger: ILogger): HubMessage | null {
if (input.length === 0) { if (input.length === 0) {
throw new Error("Invalid payload."); throw new Error("Invalid payload.");
} }
@ -173,24 +185,27 @@ export class MessagePackHubProtocol implements IHubProtocol {
throw new Error("Invalid payload for Completion message."); throw new Error("Invalid payload for Completion message.");
} }
const completionMessage = { let error: string | undefined;
error: null as string, let result: any;
headers,
invocationId: properties[2],
result: null as any,
type: MessageType.Completion,
};
switch (resultKind) { switch (resultKind) {
case errorResult: case errorResult:
completionMessage.error = properties[4]; error = properties[4];
break; break;
case nonVoidResult: case nonVoidResult:
completionMessage.result = properties[4]; result = properties[4];
break; break;
} }
return completionMessage as CompletionMessage; const completionMessage: CompletionMessage = {
error,
headers,
invocationId: properties[2],
result,
type: MessageType.Completion,
};
return completionMessage;
} }
private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer { private writeInvocation(invocationMessage: InvocationMessage): ArrayBuffer {

View File

@ -66,12 +66,10 @@ describe("MessageHubProtocol", () => {
error: "Err", error: "Err",
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: null,
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[[0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b], [[0x0b, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xa2, 0x4f, 0x4b],
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: "OK", result: "OK",
@ -79,15 +77,12 @@ describe("MessageHubProtocol", () => {
} as CompletionMessage], } as CompletionMessage],
[[0x08, 0x94, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02], [[0x08, 0x94, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02],
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: null,
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[[0x0E, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xD6, 0xFF, 0x5A, 0x4A, 0x1A, 0x50], [[0x0E, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x03, 0xD6, 0xFF, 0x5A, 0x4A, 0x1A, 0x50],
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)), result: new Date(Date.UTC(2018, 0, 1, 11, 24, 0)),
@ -96,10 +91,8 @@ describe("MessageHubProtocol", () => {
// extra property at the end should be ignored (testing older protocol client working with newer protocol server) // extra property at the end should be ignored (testing older protocol client working with newer protocol server)
[[0x09, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02, 0x00], [[0x09, 0x95, 0x03, 0x80, 0xa3, 0x61, 0x62, 0x63, 0x02, 0x00],
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: null,
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
] as Array<[number[], CompletionMessage]>).forEach(([payload, expectedMessage]) => ] as Array<[number[], CompletionMessage]>).forEach(([payload, expectedMessage]) =>
@ -174,7 +167,6 @@ describe("MessageHubProtocol", () => {
type: MessageType.StreamItem, type: MessageType.StreamItem,
} as StreamItemMessage, } as StreamItemMessage,
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: "OK", result: "OK",

View File

@ -8,7 +8,7 @@
// Not exported from index. // Not exported from index.
export class AbortController implements AbortSignal { export class AbortController implements AbortSignal {
private isAborted: boolean = false; private isAborted: boolean = false;
public onabort: () => void; public onabort: (() => void) | null = null;
public abort() { public abort() {
if (!this.isAborted) { if (!this.isAborted) {
@ -33,5 +33,5 @@ export interface AbortSignal {
/** Indicates if the request has been aborted. */ /** Indicates if the request has been aborted. */
aborted: boolean; aborted: boolean;
/** Set this to a handler that will be invoked when the request is aborted. */ /** Set this to a handler that will be invoked when the request is aborted. */
onabort: () => void; onabort: (() => void) | null;
} }

View File

@ -166,15 +166,27 @@ export class DefaultHttpClient extends HttpClient {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
if (!request.method) {
reject(new Error("No method defined."));
return;
}
if (!request.url) {
reject(new Error("No url defined."));
return;
}
xhr.open(request.method, request.url, true); xhr.open(request.method, request.url, true);
xhr.withCredentials = true; xhr.withCredentials = true;
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Explicitly setting the Content-Type header for React Native on Android platform. // Explicitly setting the Content-Type header for React Native on Android platform.
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8"); xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
if (request.headers) { const headers = request.headers;
Object.keys(request.headers) if (headers) {
.forEach((header) => xhr.setRequestHeader(header, request.headers[header])); Object.keys(headers)
.forEach((header) => {
xhr.setRequestHeader(header, headers[header]);
});
} }
if (request.responseType) { if (request.responseType) {

View File

@ -37,14 +37,14 @@ export class HttpConnection implements IConnection {
private readonly httpClient: HttpClient; private readonly httpClient: HttpClient;
private readonly logger: ILogger; private readonly logger: ILogger;
private readonly options: IHttpConnectionOptions; private readonly options: IHttpConnectionOptions;
private transport: ITransport; private transport?: ITransport;
private startPromise: Promise<void>; private startPromise?: Promise<void>;
private stopError?: Error; private stopError?: Error;
private accessTokenFactory?: () => string | Promise<string>; private accessTokenFactory?: () => string | Promise<string>;
public readonly features: any = {}; public readonly features: any = {};
public onreceive: (data: string | ArrayBuffer) => void; public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: (e?: Error) => void; public onclose: ((e?: Error) => void) | null;
constructor(url: string, options: IHttpConnectionOptions = {}) { constructor(url: string, options: IHttpConnectionOptions = {}) {
Arg.isRequired(url, "url"); Arg.isRequired(url, "url");
@ -53,12 +53,13 @@ export class HttpConnection implements IConnection {
this.baseUrl = this.resolveUrl(url); this.baseUrl = this.resolveUrl(url);
options = options || {}; options = options || {};
options.accessTokenFactory = options.accessTokenFactory || (() => null);
options.logMessageContent = options.logMessageContent || false; options.logMessageContent = options.logMessageContent || false;
this.httpClient = options.httpClient || new DefaultHttpClient(this.logger); this.httpClient = options.httpClient || new DefaultHttpClient(this.logger);
this.connectionState = ConnectionState.Disconnected; this.connectionState = ConnectionState.Disconnected;
this.options = options; this.options = options;
this.onreceive = null;
this.onclose = null;
} }
public start(): Promise<void>; public start(): Promise<void>;
@ -85,7 +86,8 @@ export class HttpConnection implements IConnection {
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); // Transport will not be null if state is connected
return this.transport!.send(data);
} }
public async stop(error?: Error): Promise<void> { public async stop(error?: Error): Promise<void> {
@ -101,7 +103,7 @@ export class HttpConnection implements IConnection {
if (this.transport) { if (this.transport) {
this.stopError = error; this.stopError = error;
await this.transport.stop(); await this.transport.stop();
this.transport = null; this.transport = undefined;
} }
} }
@ -118,12 +120,12 @@ export class HttpConnection implements IConnection {
this.transport = this.constructTransport(HttpTransportType.WebSockets); this.transport = this.constructTransport(HttpTransportType.WebSockets);
// We should just call connect directly in this case. // We should just call connect directly in this case.
// No fallback or negotiate in this case. // No fallback or negotiate in this case.
await this.transport.connect(url, transferFormat); await this.transport!.connect(url, transferFormat);
} else { } else {
throw Error("Negotiation can only be skipped when using the WebSocket transport directly."); throw Error("Negotiation can only be skipped when using the WebSocket transport directly.");
} }
} else { } else {
let negotiateResponse: INegotiateResponse = null; let negotiateResponse: INegotiateResponse | null = null;
let redirects = 0; let redirects = 0;
do { do {
@ -159,8 +161,8 @@ export class HttpConnection implements IConnection {
this.features.inherentKeepAlive = true; this.features.inherentKeepAlive = true;
} }
this.transport.onreceive = this.onreceive; this.transport!.onreceive = this.onreceive;
this.transport.onclose = (e) => this.stopConnection(e); this.transport!.onclose = (e) => this.stopConnection(e);
// only change the state if we were connecting to not overwrite // only change the state if we were connecting to not overwrite
// the state if the connection is already marked as Disconnected // the state if the connection is already marked as Disconnected
@ -168,18 +170,20 @@ export class HttpConnection implements IConnection {
} catch (e) { } catch (e) {
this.logger.log(LogLevel.Error, "Failed to start the connection: " + e); this.logger.log(LogLevel.Error, "Failed to start the connection: " + e);
this.connectionState = ConnectionState.Disconnected; this.connectionState = ConnectionState.Disconnected;
this.transport = null; this.transport = undefined;
throw e; throw e;
} }
} }
private async getNegotiationResponse(url: string): Promise<INegotiateResponse> { private async getNegotiationResponse(url: string): Promise<INegotiateResponse> {
const token = await this.accessTokenFactory();
let headers; let headers;
if (token) { if (this.accessTokenFactory) {
headers = { const token = await this.accessTokenFactory();
["Authorization"]: `Bearer ${token}`, if (token) {
}; headers = {
["Authorization"]: `Bearer ${token}`,
};
}
} }
const negotiateUrl = this.resolveNegotiateUrl(url); const negotiateUrl = this.resolveNegotiateUrl(url);
@ -201,11 +205,14 @@ export class HttpConnection implements IConnection {
} }
} }
private createConnectUrl(url: string, connectionId: string) { private createConnectUrl(url: string, connectionId: string | null | undefined) {
if (!connectionId) {
return url;
}
return url + (url.indexOf("?") === -1 ? "?" : "&") + `id=${connectionId}`; return url + (url.indexOf("?") === -1 ? "?" : "&") + `id=${connectionId}`;
} }
private async createTransport(url: string, requestedTransport: HttpTransportType | ITransport, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat): Promise<void> { private async createTransport(url: string, requestedTransport: HttpTransportType | ITransport | undefined, negotiateResponse: INegotiateResponse, requestedTransferFormat: TransferFormat): Promise<void> {
let connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId); let connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId);
if (this.isITransport(requestedTransport)) { if (this.isITransport(requestedTransport)) {
this.logger.log(LogLevel.Debug, "Connection was provided an instance of ITransport, using that directly."); this.logger.log(LogLevel.Debug, "Connection was provided an instance of ITransport, using that directly.");
@ -218,24 +225,24 @@ export class HttpConnection implements IConnection {
return; return;
} }
const transports = negotiateResponse.availableTransports; const transports = negotiateResponse.availableTransports || [];
for (const endpoint of transports) { for (const endpoint of transports) {
this.connectionState = ConnectionState.Connecting; this.connectionState = ConnectionState.Connecting;
const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat); const transport = this.resolveTransport(endpoint, requestedTransport, requestedTransferFormat);
if (typeof transport === "number") { if (typeof transport === "number") {
this.transport = this.constructTransport(transport); this.transport = this.constructTransport(transport);
if (negotiateResponse.connectionId === null) { if (!negotiateResponse.connectionId) {
negotiateResponse = await this.getNegotiationResponse(url); negotiateResponse = await this.getNegotiationResponse(url);
connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId); connectUrl = this.createConnectUrl(url, negotiateResponse.connectionId);
} }
try { try {
await this.transport.connect(connectUrl, requestedTransferFormat); await this.transport!.connect(connectUrl, requestedTransferFormat);
this.changeState(ConnectionState.Connecting, ConnectionState.Connected); this.changeState(ConnectionState.Connecting, ConnectionState.Connected);
return; return;
} catch (ex) { } catch (ex) {
this.logger.log(LogLevel.Error, `Failed to start the transport '${HttpTransportType[transport]}': ${ex}`); this.logger.log(LogLevel.Error, `Failed to start the transport '${HttpTransportType[transport]}': ${ex}`);
this.connectionState = ConnectionState.Disconnected; this.connectionState = ConnectionState.Disconnected;
negotiateResponse.connectionId = null; negotiateResponse.connectionId = undefined;
} }
} }
} }
@ -246,17 +253,17 @@ export class HttpConnection implements IConnection {
private constructTransport(transport: HttpTransportType) { private constructTransport(transport: HttpTransportType) {
switch (transport) { switch (transport) {
case HttpTransportType.WebSockets: case HttpTransportType.WebSockets:
return new WebSocketTransport(this.accessTokenFactory, this.logger, this.options.logMessageContent); return new WebSocketTransport(this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
case HttpTransportType.ServerSentEvents: case HttpTransportType.ServerSentEvents:
return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent); return new ServerSentEventsTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
case HttpTransportType.LongPolling: case HttpTransportType.LongPolling:
return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent); return new LongPollingTransport(this.httpClient, this.accessTokenFactory, this.logger, this.options.logMessageContent || false);
default: default:
throw new Error(`Unknown transport: ${transport}.`); throw new Error(`Unknown transport: ${transport}.`);
} }
} }
private resolveTransport(endpoint: IAvailableTransport, requestedTransport: HttpTransportType, requestedTransferFormat: TransferFormat): HttpTransportType | null { private resolveTransport(endpoint: IAvailableTransport, requestedTransport: HttpTransportType | undefined, requestedTransferFormat: TransferFormat): HttpTransportType | null {
const transport = HttpTransportType[endpoint.transport]; const transport = HttpTransportType[endpoint.transport];
if (transport === null || transport === undefined) { if (transport === null || transport === undefined) {
this.logger.log(LogLevel.Debug, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`); this.logger.log(LogLevel.Debug, `Skipping transport '${endpoint.transport}' because it is not supported by this client.`);
@ -294,7 +301,7 @@ export class HttpConnection implements IConnection {
} }
private async stopConnection(error?: Error): Promise<void> { private async stopConnection(error?: Error): Promise<void> {
this.transport = null; this.transport = undefined;
// If we have a stopError, it takes precedence over the error from the transport // If we have a stopError, it takes precedence over the error from the transport
error = this.stopError || error; error = this.stopError || error;
@ -346,6 +353,6 @@ export class HttpConnection implements IConnection {
} }
} }
function transportMatches(requestedTransport: HttpTransportType, actualTransport: HttpTransportType) { function transportMatches(requestedTransport: HttpTransportType | undefined, actualTransport: HttpTransportType) {
return !requestedTransport || ((actualTransport & requestedTransport) !== 0); return !requestedTransport || ((actualTransport & requestedTransport) !== 0);
} }

View File

@ -27,12 +27,12 @@ export class HubConnection {
private readonly logger: ILogger; private readonly logger: ILogger;
private protocol: IHubProtocol; private protocol: IHubProtocol;
private handshakeProtocol: HandshakeProtocol; private handshakeProtocol: HandshakeProtocol;
private callbacks: { [invocationId: string]: (invocationEvent: StreamItemMessage | CompletionMessage, error?: Error) => void }; private callbacks: { [invocationId: string]: (invocationEvent: StreamItemMessage | CompletionMessage | null, error?: Error) => void };
private methods: { [name: string]: Array<(...args: any[]) => void> }; private methods: { [name: string]: Array<(...args: any[]) => void> };
private id: number; private id: number;
private closedCallbacks: Array<(error?: Error) => void>; private closedCallbacks: Array<(error?: Error) => void>;
private timeoutHandle: NodeJS.Timer; private timeoutHandle?: NodeJS.Timer;
private pingServerHandle: NodeJS.Timer; private pingServerHandle?: NodeJS.Timer;
private receivedHandshakeResponse: boolean; private receivedHandshakeResponse: boolean;
private connectionState: HubConnectionState; private connectionState: HubConnectionState;
@ -79,6 +79,7 @@ export class HubConnection {
this.methods = {}; this.methods = {};
this.closedCallbacks = []; this.closedCallbacks = [];
this.id = 0; this.id = 0;
this.receivedHandshakeResponse = false;
this.connectionState = HubConnectionState.Disconnected; this.connectionState = HubConnectionState.Disconnected;
this.cachedPingMessage = this.protocol.writeMessage({ type: MessageType.Ping }); this.cachedPingMessage = this.protocol.writeMessage({ type: MessageType.Ping });
@ -150,20 +151,21 @@ export class HubConnection {
return this.sendMessage(cancelMessage); return this.sendMessage(cancelMessage);
}); });
this.callbacks[invocationDescriptor.invocationId] = (invocationEvent: CompletionMessage | StreamItemMessage, error?: Error) => { this.callbacks[invocationDescriptor.invocationId] = (invocationEvent: CompletionMessage | StreamItemMessage | null, error?: Error) => {
if (error) { if (error) {
subject.error(error); subject.error(error);
return; return;
} } else if (invocationEvent) {
// invocationEvent will not be null when an error is not passed to the callback
if (invocationEvent.type === MessageType.Completion) { if (invocationEvent.type === MessageType.Completion) {
if (invocationEvent.error) { if (invocationEvent.error) {
subject.error(new Error(invocationEvent.error)); subject.error(new Error(invocationEvent.error));
} else {
subject.complete();
}
} else { } else {
subject.complete(); subject.next((invocationEvent.item) as T);
} }
} else {
subject.next((invocationEvent.item) as T);
} }
}; };
@ -215,20 +217,22 @@ export class HubConnection {
const invocationDescriptor = this.createInvocation(methodName, args, false); const invocationDescriptor = this.createInvocation(methodName, args, false);
const p = new Promise<any>((resolve, reject) => { const p = new Promise<any>((resolve, reject) => {
this.callbacks[invocationDescriptor.invocationId] = (invocationEvent: StreamItemMessage | CompletionMessage, error?: Error) => { // invocationId will always have a value for a non-blocking invocation
this.callbacks[invocationDescriptor.invocationId!] = (invocationEvent: StreamItemMessage | CompletionMessage | null, error?: Error) => {
if (error) { if (error) {
reject(error); reject(error);
return; return;
} } else if (invocationEvent) {
if (invocationEvent.type === MessageType.Completion) { // invocationEvent will not be null when an error is not passed to the callback
const completionMessage = invocationEvent as CompletionMessage; if (invocationEvent.type === MessageType.Completion) {
if (completionMessage.error) { if (invocationEvent.error) {
reject(new Error(completionMessage.error)); reject(new Error(invocationEvent.error));
} else {
resolve(invocationEvent.result);
}
} else { } else {
resolve(completionMessage.result); reject(new Error(`Unexpected message type: ${invocationEvent.type}`));
} }
} else {
reject(new Error(`Unexpected message type: ${invocationEvent.type}`));
} }
}; };
@ -237,7 +241,8 @@ export class HubConnection {
this.sendMessage(message) this.sendMessage(message)
.catch((e) => { .catch((e) => {
reject(e); reject(e);
delete this.callbacks[invocationDescriptor.invocationId]; // invocationId will always have a value for a non-blocking invocation
delete this.callbacks[invocationDescriptor.invocationId!];
}); });
}); });
@ -349,7 +354,7 @@ export class HubConnection {
break; break;
case MessageType.Close: case MessageType.Close:
this.logger.log(LogLevel.Information, "Close message received from server."); this.logger.log(LogLevel.Information, "Close message received from server.");
this.connection.stop(message.error ? new Error("Server returned an error on close: " + message.error) : null); this.connection.stop(message.error ? new Error("Server returned an error on close: " + message.error) : undefined);
break; break;
default: default:
this.logger.log(LogLevel.Warning, "Invalid message type: " + message.type); this.logger.log(LogLevel.Warning, "Invalid message type: " + message.type);
@ -428,7 +433,7 @@ export class HubConnection {
Object.keys(callbacks) Object.keys(callbacks)
.forEach((key) => { .forEach((key) => {
const callback = callbacks[key]; const callback = callbacks[key];
callback(undefined, error ? error : new Error("Invocation canceled due to connection being closed.")); callback(null, error ? error : new Error("Invocation canceled due to connection being closed."));
}); });
this.cleanupTimeout(); this.cleanupTimeout();

View File

@ -14,13 +14,13 @@ import { Arg, ConsoleLogger } from "./Utils";
/** A builder for configuring {@link HubConnection} instances. */ /** A builder for configuring {@link HubConnection} instances. */
export class HubConnectionBuilder { export class HubConnectionBuilder {
/** @internal */ /** @internal */
public protocol: IHubProtocol; public protocol?: IHubProtocol;
/** @internal */ /** @internal */
public httpConnectionOptions: IHttpConnectionOptions; public httpConnectionOptions?: IHttpConnectionOptions;
/** @internal */ /** @internal */
public url: string; public url?: string;
/** @internal */ /** @internal */
public logger: ILogger; public logger?: ILogger;
/** Configures console logging for the {@link HubConnection}. /** Configures console logging for the {@link HubConnection}.
* *

View File

@ -10,6 +10,6 @@ export interface IConnection {
send(data: string | ArrayBuffer): Promise<void>; send(data: string | ArrayBuffer): Promise<void>;
stop(error?: Error): Promise<void>; stop(error?: Error): Promise<void>;
onreceive: (data: string | ArrayBuffer) => void; onreceive: ((data: string | ArrayBuffer) => void) | null;
onclose: (error?: Error) => void; onclose: ((error?: Error) => void) | null;
} }

View File

@ -27,6 +27,6 @@ export interface ITransport {
connect(url: string, transferFormat: TransferFormat): Promise<void>; connect(url: string, transferFormat: TransferFormat): Promise<void>;
send(data: any): Promise<void>; send(data: any): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
onreceive: (data: string | ArrayBuffer) => void; onreceive: ((data: string | ArrayBuffer) => void) | null;
onclose: (error?: Error) => void; onclose: ((error?: Error) => void) | null;
} }

View File

@ -11,28 +11,36 @@ import { Arg, getDataDetail, sendMessage } from "./Utils";
// Not exported from 'index', this type is internal. // Not exported from 'index', this type is internal.
export class LongPollingTransport implements ITransport { export class LongPollingTransport implements ITransport {
private readonly httpClient: HttpClient; private readonly httpClient: HttpClient;
private readonly accessTokenFactory: () => string | Promise<string>; private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
private readonly logger: ILogger; private readonly logger: ILogger;
private readonly logMessageContent: boolean; private readonly logMessageContent: boolean;
private readonly pollAbort: AbortController;
private url: string; private url?: string;
private pollXhr: XMLHttpRequest; private pollXhr?: XMLHttpRequest;
private pollAbort: AbortController;
private running: boolean; private running: boolean;
private receiving: Promise<void>; private receiving?: Promise<void>;
private closeError: Error; private closeError?: Error;
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
// This is an internal type, not exported from 'index' so this is really just internal. // This is an internal type, not exported from 'index' so this is really just internal.
public get pollAborted() { public get pollAborted() {
return this.pollAbort.aborted; return this.pollAbort.aborted;
} }
constructor(httpClient: HttpClient, accessTokenFactory: () => string | Promise<string>, logger: ILogger, logMessageContent: boolean) { constructor(httpClient: HttpClient, accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory || (() => null); this.accessTokenFactory = accessTokenFactory;
this.logger = logger; this.logger = logger;
this.pollAbort = new AbortController(); this.pollAbort = new AbortController();
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
this.running = false;
this.onreceive = null;
this.onclose = null;
} }
public async connect(url: string, transferFormat: TransferFormat): Promise<void> { public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
@ -59,7 +67,7 @@ export class LongPollingTransport implements ITransport {
pollOptions.responseType = "arraybuffer"; pollOptions.responseType = "arraybuffer";
} }
const token = await this.accessTokenFactory(); const token = await this.getAccessToken();
this.updateHeaderToken(pollOptions, token); this.updateHeaderToken(pollOptions, token);
// Make initial long polling request // Make initial long polling request
@ -71,7 +79,7 @@ export class LongPollingTransport implements ITransport {
this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`); this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`);
// Mark running as false so that the poll immediately ends and runs the close logic // Mark running as false so that the poll immediately ends and runs the close logic
this.closeError = new HttpError(response.statusText, response.statusCode); this.closeError = new HttpError(response.statusText || "", response.statusCode);
this.running = false; this.running = false;
} else { } else {
this.running = true; this.running = true;
@ -80,7 +88,18 @@ export class LongPollingTransport implements ITransport {
this.receiving = this.poll(this.url, pollOptions); this.receiving = this.poll(this.url, pollOptions);
} }
private updateHeaderToken(request: HttpRequest, token: string) { private async getAccessToken(): Promise<string | null> {
if (this.accessTokenFactory) {
return await this.accessTokenFactory();
}
return null;
}
private updateHeaderToken(request: HttpRequest, token: string | null) {
if (!request.headers) {
request.headers = {};
}
if (token) { if (token) {
// tslint:disable-next-line:no-string-literal // tslint:disable-next-line:no-string-literal
request.headers["Authorization"] = `Bearer ${token}`; request.headers["Authorization"] = `Bearer ${token}`;
@ -97,7 +116,7 @@ export class LongPollingTransport implements ITransport {
try { try {
while (this.running) { while (this.running) {
// We have to get the access token on each poll, in case it changes // We have to get the access token on each poll, in case it changes
const token = await this.accessTokenFactory(); const token = await this.getAccessToken();
this.updateHeaderToken(pollOptions, token); this.updateHeaderToken(pollOptions, token);
try { try {
@ -113,7 +132,7 @@ export class LongPollingTransport implements ITransport {
this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`); this.logger.log(LogLevel.Error, `(LongPolling transport) Unexpected response code: ${response.statusCode}`);
// Unexpected status code // Unexpected status code
this.closeError = new HttpError(response.statusText, response.statusCode); this.closeError = new HttpError(response.statusText || "", response.statusCode);
this.running = false; this.running = false;
} else { } else {
// Process the response // Process the response
@ -158,7 +177,7 @@ export class LongPollingTransport implements ITransport {
if (!this.running) { if (!this.running) {
return Promise.reject(new Error("Cannot send until the transport is connected")); return Promise.reject(new Error("Cannot send until the transport is connected"));
} }
return sendMessage(this.logger, "LongPolling", this.httpClient, this.url, this.accessTokenFactory, data, this.logMessageContent); return sendMessage(this.logger, "LongPolling", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent);
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -177,9 +196,9 @@ export class LongPollingTransport implements ITransport {
const deleteOptions: HttpRequest = { const deleteOptions: HttpRequest = {
headers: {}, headers: {},
}; };
const token = await this.accessTokenFactory(); const token = await this.getAccessToken();
this.updateHeaderToken(deleteOptions, token); this.updateHeaderToken(deleteOptions, token);
await this.httpClient.delete(this.url, deleteOptions); await this.httpClient.delete(this.url!, deleteOptions);
this.logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request sent."); this.logger.log(LogLevel.Trace, "(LongPolling transport) DELETE request sent.");
} finally { } finally {
@ -201,7 +220,4 @@ export class LongPollingTransport implements ITransport {
this.onclose(this.closeError); this.onclose(this.closeError);
} }
} }
public onreceive: (data: string | ArrayBuffer) => void;
public onclose: (error?: Error) => void;
} }

View File

@ -8,17 +8,23 @@ import { Arg, getDataDetail, sendMessage } from "./Utils";
export class ServerSentEventsTransport implements ITransport { export class ServerSentEventsTransport implements ITransport {
private readonly httpClient: HttpClient; private readonly httpClient: HttpClient;
private readonly accessTokenFactory: () => string | Promise<string>; private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
private readonly logger: ILogger; private readonly logger: ILogger;
private readonly logMessageContent: boolean; private readonly logMessageContent: boolean;
private eventSource: EventSource; private eventSource?: EventSource;
private url: string; private url?: string;
constructor(httpClient: HttpClient, accessTokenFactory: () => string | Promise<string>, logger: ILogger, logMessageContent: boolean) { 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) {
this.httpClient = httpClient; this.httpClient = httpClient;
this.accessTokenFactory = accessTokenFactory || (() => null); this.accessTokenFactory = accessTokenFactory;
this.logger = logger; this.logger = logger;
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
this.onreceive = null;
this.onclose = null;
} }
public async connect(url: string, transferFormat: TransferFormat): Promise<void> { public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
@ -32,9 +38,11 @@ export class ServerSentEventsTransport implements ITransport {
this.logger.log(LogLevel.Trace, "(SSE transport) Connecting"); this.logger.log(LogLevel.Trace, "(SSE transport) Connecting");
const token = await this.accessTokenFactory(); if (this.accessTokenFactory) {
if (token) { const token = await this.accessTokenFactory();
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`; if (token) {
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
}
} }
this.url = url; this.url = url;
@ -86,7 +94,7 @@ export class ServerSentEventsTransport implements ITransport {
if (!this.eventSource) { if (!this.eventSource) {
return Promise.reject(new Error("Cannot send until the transport is connected")); return Promise.reject(new Error("Cannot send until the transport is connected"));
} }
return sendMessage(this.logger, "SSE", this.httpClient, this.url, this.accessTokenFactory, data, this.logMessageContent); return sendMessage(this.logger, "SSE", this.httpClient, this.url!, this.accessTokenFactory, data, this.logMessageContent);
} }
public stop(): Promise<void> { public stop(): Promise<void> {
@ -97,14 +105,11 @@ export class ServerSentEventsTransport implements ITransport {
private close(e?: Error) { private close(e?: Error) {
if (this.eventSource) { if (this.eventSource) {
this.eventSource.close(); this.eventSource.close();
this.eventSource = null; this.eventSource = undefined;
if (this.onclose) { if (this.onclose) {
this.onclose(e); this.onclose(e);
} }
} }
} }
public onreceive: (data: string | ArrayBuffer) => void;
public onclose: (error?: Error) => void;
} }

View File

@ -22,19 +22,19 @@ export class Arg {
} }
export function getDataDetail(data: any, includeContent: boolean): string { export function getDataDetail(data: any, includeContent: boolean): string {
let length: string = null; let detail = "";
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
length = `Binary data of length ${data.byteLength}`; detail = `Binary data of length ${data.byteLength}`;
if (includeContent) { if (includeContent) {
length += `. Content: '${formatArrayBuffer(data)}'`; detail += `. Content: '${formatArrayBuffer(data)}'`;
} }
} else if (typeof data === "string") { } else if (typeof data === "string") {
length = `String data of length ${data.length}`; detail = `String data of length ${data.length}`;
if (includeContent) { if (includeContent) {
length += `. Content: '${data}'.`; detail += `. Content: '${data}'.`;
} }
} }
return length; return detail;
} }
export function formatArrayBuffer(data: ArrayBuffer): string { export function formatArrayBuffer(data: ArrayBuffer): string {
@ -51,13 +51,15 @@ export function formatArrayBuffer(data: ArrayBuffer): string {
return str.substr(0, str.length - 1); return str.substr(0, str.length - 1);
} }
export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: () => string | Promise<string>, content: string | ArrayBuffer, logMessageContent: boolean): Promise<void> { export async function sendMessage(logger: ILogger, transportName: string, httpClient: HttpClient, url: string, accessTokenFactory: (() => string | Promise<string>) | undefined, content: string | ArrayBuffer, logMessageContent: boolean): Promise<void> {
let headers; let headers;
const token = await accessTokenFactory(); if (accessTokenFactory) {
if (token) { const token = await accessTokenFactory();
headers = { if (token) {
["Authorization"]: `Bearer ${token}`, headers = {
}; ["Authorization"]: `Bearer ${token}`,
};
}
} }
logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`); logger.log(LogLevel.Trace, `(${transportName} transport) sending data. ${getDataDetail(content, logMessageContent)}.`);

View File

@ -7,14 +7,20 @@ import { Arg, getDataDetail } from "./Utils";
export class WebSocketTransport implements ITransport { export class WebSocketTransport implements ITransport {
private readonly logger: ILogger; private readonly logger: ILogger;
private readonly accessTokenFactory: () => string | Promise<string>; private readonly accessTokenFactory: (() => string | Promise<string>) | undefined;
private readonly logMessageContent: boolean; private readonly logMessageContent: boolean;
private webSocket: WebSocket; private webSocket?: WebSocket;
constructor(accessTokenFactory: () => string | Promise<string>, logger: ILogger, logMessageContent: boolean) { public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
constructor(accessTokenFactory: (() => string | Promise<string>) | undefined, logger: ILogger, logMessageContent: boolean) {
this.logger = logger; this.logger = logger;
this.accessTokenFactory = accessTokenFactory || (() => null); this.accessTokenFactory = accessTokenFactory;
this.logMessageContent = logMessageContent; this.logMessageContent = logMessageContent;
this.onreceive = null;
this.onclose = null;
} }
public async connect(url: string, transferFormat: TransferFormat): Promise<void> { public async connect(url: string, transferFormat: TransferFormat): Promise<void> {
@ -28,9 +34,11 @@ export class WebSocketTransport implements ITransport {
this.logger.log(LogLevel.Trace, "(WebSockets transport) Connecting"); this.logger.log(LogLevel.Trace, "(WebSockets transport) Connecting");
const token = await this.accessTokenFactory(); if (this.accessTokenFactory) {
if (token) { const token = await this.accessTokenFactory();
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`; if (token) {
url += (url.indexOf("?") < 0 ? "?" : "&") + `access_token=${encodeURIComponent(token)}`;
}
} }
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
@ -46,8 +54,9 @@ export class WebSocketTransport implements ITransport {
resolve(); resolve();
}; };
webSocket.onerror = (event: ErrorEvent) => { webSocket.onerror = (event: Event) => {
reject(event.error); const error = (event instanceof ErrorEvent) ? event.error : null;
reject(error);
}; };
webSocket.onmessage = (message: MessageEvent) => { webSocket.onmessage = (message: MessageEvent) => {
@ -84,11 +93,8 @@ export class WebSocketTransport implements ITransport {
public stop(): Promise<void> { public stop(): Promise<void> {
if (this.webSocket) { if (this.webSocket) {
this.webSocket.close(); this.webSocket.close();
this.webSocket = null; this.webSocket = undefined;
} }
return Promise.resolve(); return Promise.resolve();
} }
public onreceive: (data: string | ArrayBuffer) => void;
public onclose: (error?: Error) => void;
} }

View File

@ -7,9 +7,10 @@ import { TestHttpClient } from "./TestHttpClient";
describe("HttpClient", () => { describe("HttpClient", () => {
describe("get", () => { describe("get", () => {
it("sets the method and URL appropriately", async () => { it("sets the method and URL appropriately", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.get("http://localhost"); await testClient.get("http://localhost");
@ -18,9 +19,10 @@ describe("HttpClient", () => {
}); });
it("overrides method and url in options", async () => { it("overrides method and url in options", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.get("http://localhost", { await testClient.get("http://localhost", {
@ -32,9 +34,10 @@ describe("HttpClient", () => {
}); });
it("copies other options", async () => { it("copies other options", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.get("http://localhost", { await testClient.get("http://localhost", {
@ -46,9 +49,10 @@ describe("HttpClient", () => {
describe("post", () => { describe("post", () => {
it("sets the method and URL appropriately", async () => { it("sets the method and URL appropriately", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.post("http://localhost"); await testClient.post("http://localhost");
@ -57,9 +61,10 @@ describe("HttpClient", () => {
}); });
it("overrides method and url in options", async () => { it("overrides method and url in options", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.post("http://localhost", { await testClient.post("http://localhost", {
@ -71,9 +76,10 @@ describe("HttpClient", () => {
}); });
it("copies other options", async () => { it("copies other options", async () => {
let request: HttpRequest; let request!: HttpRequest;
const testClient = new TestHttpClient().on((r) => { const testClient = new TestHttpClient().on((r) => {
request = r; return ""; request = r;
return "";
}); });
await testClient.post("http://localhost", { await testClient.post("http://localhost", {

View File

@ -13,7 +13,6 @@ import { TestHttpClient } from "./TestHttpClient";
import { PromiseSource } from "./Utils"; import { PromiseSource } from "./Utils";
const commonOptions: IHttpConnectionOptions = { const commonOptions: IHttpConnectionOptions = {
logger: null,
}; };
const defaultConnectionId = "abc123"; const defaultConnectionId = "abc123";
@ -165,8 +164,8 @@ describe("HttpConnection", () => {
stop(): Promise<void> { stop(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
}, },
onclose: undefined, onclose: null,
onreceive: undefined, onreceive: null,
}; };
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
@ -224,7 +223,7 @@ describe("HttpConnection", () => {
const negotiateResponse = { ...defaultNegotiateResponse }; const negotiateResponse = { ...defaultNegotiateResponse };
// Remove the requested transport from the response // Remove the requested transport from the response
negotiateResponse.availableTransports = negotiateResponse.availableTransports negotiateResponse.availableTransports = negotiateResponse.availableTransports!
.filter((f) => f.transport !== HttpTransportType[requestedTransport]); .filter((f) => f.transport !== HttpTransportType[requestedTransport]);
const options: IHttpConnectionOptions = { const options: IHttpConnectionOptions = {
@ -483,7 +482,7 @@ describe("HttpConnection", () => {
.on("GET", (r) => { .on("GET", (r) => {
httpClientGetCount++; httpClientGetCount++;
// tslint:disable-next-line:no-string-literal // tslint:disable-next-line:no-string-literal
const authorizationValue = r.headers["Authorization"]; const authorizationValue = r.headers!["Authorization"];
if (httpClientGetCount === 1) { if (httpClientGetCount === 1) {
if (authorizationValue) { if (authorizationValue) {
fail("First long poll request should have a authorization header."); fail("First long poll request should have a authorization header.");

View File

@ -13,7 +13,7 @@ import { TextMessageFormat } from "../src/TextMessageFormat";
import { delay, PromiseSource } from "./Utils"; import { delay, PromiseSource } from "./Utils";
function createHubConnection(connection: IConnection, logger?: ILogger, protocol?: IHubProtocol) { function createHubConnection(connection: IConnection, logger?: ILogger | null, protocol?: IHubProtocol | null) {
return HubConnection.create(connection, logger || NullLogger.instance, protocol || new JsonHubProtocol()); return HubConnection.create(connection, logger || NullLogger.instance, protocol || new JsonHubProtocol());
} }
@ -181,7 +181,7 @@ describe("HubConnection", () => {
}); });
it("can process handshake and additional messages from binary", async () => { it("can process handshake and additional messages from binary", async () => {
let receivedProcotolData: ArrayBuffer; let receivedProcotolData: ArrayBuffer | undefined;
const mockProtocol = new TestProtocol(TransferFormat.Binary); const mockProtocol = new TestProtocol(TransferFormat.Binary);
mockProtocol.onreceive = (d) => receivedProcotolData = d as ArrayBuffer; mockProtocol.onreceive = (d) => receivedProcotolData = d as ArrayBuffer;
@ -202,14 +202,14 @@ describe("HubConnection", () => {
connection.receiveBinary(new Uint8Array(data).buffer); connection.receiveBinary(new Uint8Array(data).buffer);
// left over data is the message pack message // left over data is the message pack message
expect(receivedProcotolData.byteLength).toEqual(102); expect(receivedProcotolData!.byteLength).toEqual(102);
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
}); });
it("can process handshake and additional messages from text", async () => { it("can process handshake and additional messages from text", async () => {
let receivedProcotolData: string; let receivedProcotolData: string | undefined;
const mockProtocol = new TestProtocol(TransferFormat.Text); const mockProtocol = new TestProtocol(TransferFormat.Text);
mockProtocol.onreceive = (d) => receivedProcotolData = d as string; mockProtocol.onreceive = (d) => receivedProcotolData = d as string;
@ -281,7 +281,7 @@ describe("HubConnection", () => {
const invokePromise = hubConnection.invoke("testMethod"); const invokePromise = hubConnection.invoke("testMethod");
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(new Error("Connection lost")); connection.onclose!(new Error("Connection lost"));
expect(invokePromise).rejects.toThrow("Connection lost"); expect(invokePromise).rejects.toThrow("Connection lost");
} finally { } finally {
@ -474,12 +474,12 @@ describe("HubConnection", () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
let closeError: Error = null; let closeError: Error | undefined;
hubConnection.onclose((e) => closeError = e); hubConnection.onclose((e) => closeError = e);
connection.receiveHandshakeResponse("Error!"); connection.receiveHandshakeResponse("Error!");
expect(closeError.message).toEqual("Server returned handshake error: Error!"); expect(closeError!.message).toEqual("Server returned handshake error: Error!");
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -490,7 +490,7 @@ describe("HubConnection", () => {
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
let isClosed = false; let isClosed = false;
let closeError: Error = null; let closeError: Error | undefined;
hubConnection.onclose((e) => { hubConnection.onclose((e) => {
isClosed = true; isClosed = true;
closeError = e; closeError = e;
@ -503,7 +503,7 @@ describe("HubConnection", () => {
}); });
expect(isClosed).toEqual(true); expect(isClosed).toEqual(true);
expect(closeError).toEqual(null); expect(closeError).toBeUndefined();
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -514,7 +514,7 @@ describe("HubConnection", () => {
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
let isClosed = false; let isClosed = false;
let closeError: Error = null; let closeError: Error | undefined;
hubConnection.onclose((e) => { hubConnection.onclose((e) => {
isClosed = true; isClosed = true;
closeError = e; closeError = e;
@ -528,7 +528,7 @@ describe("HubConnection", () => {
}); });
expect(isClosed).toEqual(true); expect(isClosed).toEqual(true);
expect(closeError.message).toEqual("Server returned an error on close: Error!"); expect(closeError!.message).toEqual("Server returned an error on close: Error!");
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -622,12 +622,12 @@ describe("HubConnection", () => {
try { try {
connection.receiveHandshakeResponse(); connection.receiveHandshakeResponse();
hubConnection.on(null, undefined); hubConnection.on(null!, undefined!);
hubConnection.on(undefined, null); hubConnection.on(undefined!, null!);
hubConnection.on("message", null); hubConnection.on("message", null!);
hubConnection.on("message", undefined); hubConnection.on("message", undefined!);
hubConnection.on(null, () => { }); hubConnection.on(null!, () => { });
hubConnection.on(undefined, () => { }); hubConnection.on(undefined!, () => { });
// invoke a method to make sure we are not trying to use null/undefined // invoke a method to make sure we are not trying to use null/undefined
connection.receive({ connection.receive({
@ -640,12 +640,12 @@ describe("HubConnection", () => {
expect(warnings).toEqual(["No client method with the name 'message' found."]); expect(warnings).toEqual(["No client method with the name 'message' found."]);
hubConnection.off(null, undefined); hubConnection.off(null!, undefined!);
hubConnection.off(undefined, null); hubConnection.off(undefined!, null!);
hubConnection.off("message", null); hubConnection.off("message", null!);
hubConnection.off("message", undefined); hubConnection.off("message", undefined!);
hubConnection.off(null, () => { }); hubConnection.off(null!, () => { });
hubConnection.off(undefined, () => { }); hubConnection.off(undefined!, () => { });
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -741,7 +741,7 @@ describe("HubConnection", () => {
.subscribe(observer); .subscribe(observer);
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(new Error("Connection lost")); connection.onclose!(new Error("Connection lost"));
expect(observer.completed).rejects.toThrow("Error: Connection lost"); expect(observer.completed).rejects.toThrow("Error: Connection lost");
} finally { } finally {
@ -784,7 +784,7 @@ describe("HubConnection", () => {
// Typically this would be called by the transport // Typically this would be called by the transport
// triggers observer.error() // triggers observer.error()
connection.onclose(new Error("Connection lost")); connection.onclose!(new Error("Connection lost"));
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -845,7 +845,7 @@ describe("HubConnection", () => {
hubConnection.onclose((e) => invocations++); hubConnection.onclose((e) => invocations++);
hubConnection.onclose((e) => invocations++); hubConnection.onclose((e) => invocations++);
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(); connection.onclose!();
expect(invocations).toBe(2); expect(invocations).toBe(2);
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
@ -856,12 +856,12 @@ describe("HubConnection", () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
let error: Error; let error: Error | undefined;
hubConnection.onclose((e) => error = e); hubConnection.onclose((e) => error = e);
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(new Error("Test error.")); connection.onclose!(new Error("Test error."));
expect(error.message).toBe("Test error."); expect(error!.message).toBe("Test error.");
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
} }
@ -871,10 +871,10 @@ describe("HubConnection", () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
hubConnection.onclose(null); hubConnection.onclose(null!);
hubConnection.onclose(undefined); hubConnection.onclose(undefined!);
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(); connection.onclose!();
// expect no errors // expect no errors
} finally { } finally {
hubConnection.stop(); hubConnection.stop();
@ -885,10 +885,10 @@ describe("HubConnection", () => {
const connection = new TestConnection(); const connection = new TestConnection();
const hubConnection = createHubConnection(connection); const hubConnection = createHubConnection(connection);
try { try {
let state: HubConnectionState; let state: HubConnectionState | undefined;
hubConnection.onclose((e) => state = hubConnection.state); hubConnection.onclose((e) => state = hubConnection.state);
// Typically this would be called by the transport // Typically this would be called by the transport
connection.onclose(); connection.onclose!();
expect(state).toBe(HubConnectionState.Disconnected); expect(state).toBe(HubConnectionState.Disconnected);
} finally { } finally {
@ -998,6 +998,18 @@ async function pingAndWait(connection: TestConnection): Promise<void> {
class TestConnection implements IConnection { class TestConnection implements IConnection {
public readonly features: any = {}; public readonly features: any = {};
public onreceive: ((data: string | ArrayBuffer) => void) | null;
public onclose: ((error?: Error) => void) | null;
public sentData: any[];
public lastInvocationId: string | null;
constructor() {
this.onreceive = null;
this.onclose = null;
this.sentData = [];
this.lastInvocationId = null;
}
public start(): Promise<void> { public start(): Promise<void> {
return Promise.resolve(); return Promise.resolve();
} }
@ -1029,21 +1041,22 @@ class TestConnection implements IConnection {
public receive(data: any): void { public receive(data: any): void {
const payload = JSON.stringify(data); const payload = JSON.stringify(data);
this.onreceive(TextMessageFormat.write(payload)); this.invokeOnReceive(TextMessageFormat.write(payload));
} }
public receiveText(data: string) { public receiveText(data: string) {
this.onreceive(data); this.invokeOnReceive(data);
} }
public receiveBinary(data: ArrayBuffer) { public receiveBinary(data: ArrayBuffer) {
this.onreceive(data); this.invokeOnReceive(data);
} }
public onreceive: (data: string | ArrayBuffer) => void; private invokeOnReceive(data: string | ArrayBuffer) {
public onclose: (error?: Error) => void; if (this.onreceive) {
public sentData: any[]; this.onreceive(data);
public lastInvocationId: string; }
}
} }
class TestProtocol implements IHubProtocol { class TestProtocol implements IHubProtocol {
@ -1052,10 +1065,11 @@ class TestProtocol implements IHubProtocol {
public readonly transferFormat: TransferFormat; public readonly transferFormat: TransferFormat;
public onreceive: (data: string | ArrayBuffer) => void; public onreceive: ((data: string | ArrayBuffer) => void) | null;
constructor(transferFormat: TransferFormat) { constructor(transferFormat: TransferFormat) {
this.transferFormat = transferFormat; this.transferFormat = transferFormat;
this.onreceive = null;
} }
public parseMessages(input: any): HubMessage[] { public parseMessages(input: any): HubMessage[] {
@ -1072,17 +1086,17 @@ class TestProtocol implements IHubProtocol {
} }
class TestObserver implements IStreamSubscriber<any> { class TestObserver implements IStreamSubscriber<any> {
public readonly closed: boolean; public readonly closed: boolean = false;
public itemsReceived: [any]; public itemsReceived: any[];
private itemsSource: PromiseSource<[any]>; private itemsSource: PromiseSource<any[]>;
get completed(): Promise<[any]> { get completed(): Promise<any[]> {
return this.itemsSource.promise; return this.itemsSource.promise;
} }
constructor() { constructor() {
this.itemsReceived = [] as [any]; this.itemsReceived = [];
this.itemsSource = new PromiseSource<[any]>(); this.itemsSource = new PromiseSource<any[]>();
} }
public next(value: any) { public next(value: any) {

View File

@ -37,17 +37,17 @@ describe("HubConnectionBuilder", () => {
eachMissingValue((val, name) => { eachMissingValue((val, name) => {
it(`configureLogging throws if logger is ${name}`, () => { it(`configureLogging throws if logger is ${name}`, () => {
const builder = new HubConnectionBuilder(); const builder = new HubConnectionBuilder();
expect(() => builder.configureLogging(val)).toThrow("The 'logging' argument is required."); expect(() => builder.configureLogging(val!)).toThrow("The 'logging' argument is required.");
}); });
it(`withUrl throws if url is ${name}`, () => { it(`withUrl throws if url is ${name}`, () => {
const builder = new HubConnectionBuilder(); const builder = new HubConnectionBuilder();
expect(() => builder.withUrl(val)).toThrow("The 'url' argument is required."); expect(() => builder.withUrl(val!)).toThrow("The 'url' argument is required.");
}); });
it(`withHubProtocol throws if protocol is ${name}`, () => { it(`withHubProtocol throws if protocol is ${name}`, () => {
const builder = new HubConnectionBuilder(); const builder = new HubConnectionBuilder();
expect(() => builder.withHubProtocol(val)).toThrow("The 'protocol' argument is required."); expect(() => builder.withHubProtocol(val!)).toThrow("The 'protocol' argument is required.");
}); });
}); });
@ -88,7 +88,7 @@ describe("HubConnectionBuilder", () => {
const builder = createConnectionBuilder() const builder = createConnectionBuilder()
.withUrl("http://example.com", HttpTransportType.WebSockets) .withUrl("http://example.com", HttpTransportType.WebSockets)
.withHubProtocol(protocol); .withHubProtocol(protocol);
expect(builder.httpConnectionOptions.transport).toBe(HttpTransportType.WebSockets); expect(builder.httpConnectionOptions!.transport).toBe(HttpTransportType.WebSockets);
}); });
it("can configure hub protocol", async () => { it("can configure hub protocol", async () => {

View File

@ -71,33 +71,29 @@ describe("JsonHubProtocol", () => {
result: null, result: null,
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[`{"type":3, "invocationId": "abc", "result": "OK", "error": null, "headers": {}}${TextMessageFormat.RecordSeparator}`, [`{"type":3, "invocationId": "abc", "result": "OK", "headers": {}}${TextMessageFormat.RecordSeparator}`,
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: "OK", result: "OK",
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[`{"type":3, "invocationId": "abc", "error": null, "result": null, "headers": {}}${TextMessageFormat.RecordSeparator}`, [`{"type":3, "invocationId": "abc", "result": null, "headers": {}}${TextMessageFormat.RecordSeparator}`,
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: null, result: null,
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[`{"type":3, "invocationId": "abc", "result": 1514805840000, "error": null, "headers": {}}${TextMessageFormat.RecordSeparator}`, [`{"type":3, "invocationId": "abc", "result": 1514805840000, "headers": {}}${TextMessageFormat.RecordSeparator}`,
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: Date.UTC(2018, 0, 1, 11, 24, 0), result: Date.UTC(2018, 0, 1, 11, 24, 0),
type: MessageType.Completion, type: MessageType.Completion,
} as CompletionMessage], } as CompletionMessage],
[`{"type":3, "invocationId": "abc", "error": null, "result": null, "headers": {}, "extraParameter":"value"}${TextMessageFormat.RecordSeparator}`, [`{"type":3, "invocationId": "abc", "result": null, "headers": {}, "extraParameter":"value"}${TextMessageFormat.RecordSeparator}`,
{ {
error: null,
extraParameter: "value", extraParameter: "value",
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
@ -165,7 +161,7 @@ describe("JsonHubProtocol", () => {
})); }));
it("can read multiple messages", () => { it("can read multiple messages", () => {
const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK", "error": null}${TextMessageFormat.RecordSeparator}`; const payload = `{"type":2, "invocationId": "abc", "headers": {}, "item": 8}${TextMessageFormat.RecordSeparator}{"type":3, "invocationId": "abc", "headers": {}, "result": "OK"}${TextMessageFormat.RecordSeparator}`;
const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance); const messages = new JsonHubProtocol().parseMessages(payload, NullLogger.instance);
expect(messages).toEqual([ expect(messages).toEqual([
{ {
@ -175,7 +171,6 @@ describe("JsonHubProtocol", () => {
type: MessageType.StreamItem, type: MessageType.StreamItem,
} as StreamItemMessage, } as StreamItemMessage,
{ {
error: null,
headers: {}, headers: {},
invocationId: "abc", invocationId: "abc",
result: "OK", result: "OK",

View File

@ -20,7 +20,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(200); return new HttpResponse(200);
} else { } else {
// Turn 'onabort' into a promise. // Turn 'onabort' into a promise.
const abort = new Promise((resolve, reject) => r.abortSignal.onabort = resolve); const abort = new Promise((resolve, reject) => r.abortSignal!.onabort = resolve);
await abort; await abort;
// Signal that the poll has completed. // Signal that the poll has completed.
@ -30,7 +30,7 @@ describe("LongPollingTransport", () => {
} }
}) })
.on("DELETE", (r) => new HttpResponse(202)); .on("DELETE", (r) => new HttpResponse(202));
const transport = new LongPollingTransport(client, null, NullLogger.instance, false); const transport = new LongPollingTransport(client, undefined, NullLogger.instance, false);
await transport.connect("http://example.com", TransferFormat.Text); await transport.connect("http://example.com", TransferFormat.Text);
const stopPromise = transport.stop(); const stopPromise = transport.stop();
@ -53,7 +53,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(204); return new HttpResponse(204);
} }
}); });
const transport = new LongPollingTransport(client, null, NullLogger.instance, false); const transport = new LongPollingTransport(client, undefined, NullLogger.instance, false);
const stopPromise = makeClosedPromise(transport); const stopPromise = makeClosedPromise(transport);
@ -84,7 +84,7 @@ describe("LongPollingTransport", () => {
return new HttpResponse(202); return new HttpResponse(202);
}); });
const transport = new LongPollingTransport(httpClient, null, NullLogger.instance, false); const transport = new LongPollingTransport(httpClient, undefined, NullLogger.instance, false);
await transport.connect("http://tempuri.org", TransferFormat.Text); await transport.connect("http://tempuri.org", TransferFormat.Text);

View File

@ -49,8 +49,8 @@ export class TestHttpClient extends HttpClient {
const oldHandler = this.handler; const oldHandler = this.handler;
const newHandler = async (request: HttpRequest) => { const newHandler = async (request: HttpRequest) => {
if (matches(method, request.method) && matches(url, request.url)) { if (matches(method, request.method!) && matches(url, request.url!)) {
const promise = handler(request, oldHandler); const promise = handler!(request, oldHandler);
let val: TestHttpHandlerResult; let val: TestHttpHandlerResult;
if (promise instanceof Promise) { if (promise instanceof Promise) {

View File

@ -12,8 +12,8 @@ export function delay(durationInMilliseconds: number): Promise<void> {
export class PromiseSource<T = void> implements Promise<T> { export class PromiseSource<T = void> implements Promise<T> {
public promise: Promise<T>; public promise: Promise<T>;
private resolver: (value?: T | PromiseLike<T>) => void; private resolver!: (value?: T | PromiseLike<T>) => void;
private rejecter: (reason?: any) => void; private rejecter!: (reason?: any) => void;
constructor() { constructor() {
this.promise = new Promise<T>((resolve, reject) => { this.promise = new Promise<T>((resolve, reject) => {

View File

@ -14,6 +14,7 @@
"suppressImplicitAnyIndexErrors": true, "suppressImplicitAnyIndexErrors": true,
"noEmitOnError": true, "noEmitOnError": true,
"stripInternal": true, "stripInternal": true,
"strict": true,
"lib": [ "es5", "es2015.promise", "es2015.iterable", "dom" ], "lib": [ "es5", "es2015.promise", "es2015.iterable", "dom" ],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {