Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Commit f4bb309

Browse files
authored
Implement #1162 by adding client timeout for JavaScript (#1163)
1 parent ae9c3cf commit f4bb309

File tree

9 files changed

+151
-45
lines changed

9 files changed

+151
-45
lines changed

client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Connection.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ describe("Connection", () => {
320320
// mode: TransferMode : TransferMode.Text
321321
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> { return Promise.resolve(transportTransferMode); },
322322
send(data: any): Promise<void> { return Promise.resolve(); },
323-
stop(): void {},
323+
stop(): void { },
324324
onreceive: null,
325325
onclose: null,
326326
mode: transportTransferMode

client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/HubConnection.spec.ts

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { TextMessageFormat } from "../Microsoft.AspNetCore.SignalR.Client.TS/For
1010
import { ILogger, LogLevel } from "../Microsoft.AspNetCore.SignalR.Client.TS/ILogger"
1111
import { MessageType } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHubProtocol"
1212

13-
import { asyncit as it, captureException } from './JasmineUtils';
13+
import { asyncit as it, captureException, delay, PromiseSource } from './Utils';
14+
import { IHubConnectionOptions } from "../Microsoft.AspNetCore.SignalR.Client.TS/IHubConnectionOptions";
1415

1516
describe("HubConnection", () => {
1617

@@ -437,7 +438,46 @@ describe("HubConnection", () => {
437438
connection.receive({ type: MessageType.Completion, invocationId: connection.lastInvocationId, result: "foo" });
438439

439440
expect(await invokePromise).toBe("foo");
440-
})
441+
});
442+
443+
it("does not terminate if messages are received", async () => {
444+
let connection = new TestConnection();
445+
let hubConnection = new HubConnection(connection, { serverTimeoutInMilliseconds: 100 });
446+
447+
let p = new PromiseSource<Error>();
448+
hubConnection.onclose(error => p.resolve(error));
449+
450+
await hubConnection.start();
451+
452+
await connection.receive({ type: MessageType.Ping });
453+
await delay(50);
454+
await connection.receive({ type: MessageType.Ping });
455+
await delay(50);
456+
await connection.receive({ type: MessageType.Ping });
457+
await delay(50);
458+
await connection.receive({ type: MessageType.Ping });
459+
await delay(50);
460+
461+
connection.stop();
462+
463+
let error = await p.promise;
464+
465+
expect(error).toBeUndefined();
466+
});
467+
468+
it("terminates if no messages received within timeout interval", async () => {
469+
let connection = new TestConnection();
470+
let hubConnection = new HubConnection(connection, { serverTimeoutInMilliseconds: 100 });
471+
472+
let p = new PromiseSource<Error>();
473+
hubConnection.onclose(error => p.resolve(error));
474+
475+
await hubConnection.start();
476+
477+
let error = await p.promise;
478+
479+
expect(error).toEqual(new Error("Server timeout elapsed without receiving a message from the server."));
480+
});
441481
})
442482
});
443483

@@ -463,9 +503,9 @@ class TestConnection implements IConnection {
463503
return Promise.resolve();
464504
};
465505

466-
stop(): void {
506+
stop(error?: Error): void {
467507
if (this.onclose) {
468-
this.onclose();
508+
this.onclose(error);
469509
}
470510
};
471511

@@ -505,26 +545,4 @@ class TestObserver implements Observer<any>
505545
complete() {
506546
this.itemsSource.resolve(this.itemsReceived);
507547
}
508-
};
509-
510-
class PromiseSource<T> {
511-
public promise: Promise<T>
512-
513-
private resolver: (value?: T | PromiseLike<T>) => void;
514-
private rejecter: (reason?: any) => void;
515-
516-
constructor() {
517-
this.promise = new Promise<T>((resolve, reject) => {
518-
this.resolver = resolve;
519-
this.rejecter = reject;
520-
});
521-
}
522-
523-
resolve(value?: T | PromiseLike<T>) {
524-
this.resolver(value);
525-
}
526-
527-
reject(reason?: any) {
528-
this.rejecter(reason);
529-
}
530-
}
548+
};

client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/JasmineUtils.ts renamed to client-ts/Microsoft.AspNetCore.SignalR.Client.TS.Tests/Utils.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
import { clearTimeout, setTimeout } from "timers";
5+
46
export function asyncit(expectation: string, assertion?: () => Promise<any>, timeout?: number): void {
57
let testFunction: (done: DoneFn) => void;
68
if (assertion) {
@@ -24,4 +26,32 @@ export async function captureException(fn: () => Promise<any>): Promise<Error> {
2426
} catch (e) {
2527
return e;
2628
}
29+
}
30+
31+
export function delay(durationInMilliseconds: number): Promise<void> {
32+
let source = new PromiseSource<void>();
33+
setTimeout(() => source.resolve(), durationInMilliseconds);
34+
return source.promise;
35+
}
36+
37+
export class PromiseSource<T> {
38+
public promise: Promise<T>
39+
40+
private resolver: (value?: T | PromiseLike<T>) => void;
41+
private rejecter: (reason?: any) => void;
42+
43+
constructor() {
44+
this.promise = new Promise<T>((resolve, reject) => {
45+
this.resolver = resolve;
46+
this.rejecter = reject;
47+
});
48+
}
49+
50+
resolve(value?: T | PromiseLike<T>) {
51+
this.resolver(value);
52+
}
53+
54+
reject(reason?: any) {
55+
this.rejecter(reason);
56+
}
2757
}

client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class HttpConnection implements IConnection {
8989
? TransferMode.Binary
9090
: TransferMode.Text;
9191

92-
this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode);
92+
this.features.transferMode = await this.transport.connect(this.url, requestedTransferMode, this);
9393

9494
// only change the state if we were connecting to not overwrite
9595
// the state if the connection is already marked as Disconnected
@@ -144,7 +144,7 @@ export class HttpConnection implements IConnection {
144144
return this.transport.send(data);
145145
}
146146

147-
async stop(): Promise<void> {
147+
async stop(error? : Error): Promise<void> {
148148
let previousState = this.connectionState;
149149
this.connectionState = ConnectionState.Disconnected;
150150

@@ -154,10 +154,10 @@ export class HttpConnection implements IConnection {
154154
catch (e) {
155155
// this exception is returned to the user as a rejected Promise from the start method
156156
}
157-
this.stopConnection(/*raiseClosed*/ previousState == ConnectionState.Connected);
157+
this.stopConnection(/*raiseClosed*/ previousState == ConnectionState.Connected, error);
158158
}
159159

160-
private stopConnection(raiseClosed: Boolean, error?: any) {
160+
private stopConnection(raiseClosed: Boolean, error?: Error) {
161161
if (this.transport) {
162162
this.transport.stop();
163163
this.transport = null;
@@ -209,4 +209,4 @@ export class HttpConnection implements IConnection {
209209

210210
onreceive: DataReceived;
211211
onclose: ConnectionClosed;
212-
}
212+
}

client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HubConnection.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ import { Base64EncodedHubProtocol } from "./Base64EncodedHubProtocol"
1313
import { ILogger, LogLevel } from "./ILogger"
1414
import { ConsoleLogger, NullLogger, LoggerFactory } from "./Loggers"
1515
import { IHubConnectionOptions } from "./IHubConnectionOptions"
16+
import { setTimeout, clearTimeout } from "timers";
1617

1718
export { TransportType } from "./Transports"
1819
export { HttpConnection } from "./HttpConnection"
1920
export { JsonHubProtocol } from "./JsonHubProtocol"
2021
export { LogLevel, ILogger } from "./ILogger"
2122
export { ConsoleLogger, NullLogger } from "./Loggers"
2223

24+
const DEFAULT_SERVER_TIMEOUT_IN_MS: number = 30 * 1000;
25+
2326
export class HubConnection {
2427
private readonly connection: IConnection;
2528
private readonly logger: ILogger;
@@ -28,9 +31,14 @@ export class HubConnection {
2831
private methods: Map<string, ((...args: any[]) => void)[]>;
2932
private id: number;
3033
private closedCallbacks: ConnectionClosed[];
34+
private timeoutHandle: NodeJS.Timer;
35+
private serverTimeoutInMilliseconds: number;
3136

3237
constructor(urlOrConnection: string | IConnection, options: IHubConnectionOptions = {}) {
3338
options = options || {};
39+
40+
this.serverTimeoutInMilliseconds = options.serverTimeoutInMilliseconds || DEFAULT_SERVER_TIMEOUT_IN_MS;
41+
3442
if (typeof urlOrConnection === "string") {
3543
this.connection = new HttpConnection(urlOrConnection, options);
3644
}
@@ -51,6 +59,10 @@ export class HubConnection {
5159
}
5260

5361
private processIncomingData(data: any) {
62+
if (this.timeoutHandle !== undefined) {
63+
clearTimeout(this.timeoutHandle);
64+
}
65+
5466
// Parse the messages
5567
let messages = this.protocol.parseMessages(data);
5668

@@ -79,6 +91,21 @@ export class HubConnection {
7991
break;
8092
}
8193
}
94+
95+
this.configureTimeout();
96+
}
97+
98+
private configureTimeout() {
99+
if (!this.connection.features || !this.connection.features.inherentKeepAlive) {
100+
// Set the timeout timer
101+
this.timeoutHandle = setTimeout(() => this.serverTimeout(), this.serverTimeoutInMilliseconds);
102+
}
103+
}
104+
105+
private serverTimeout() {
106+
// The server hasn't talked to us in a while. It doesn't like us anymore ... :(
107+
// Terminate the connection
108+
this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."));
82109
}
83110

84111
private invokeClientMethod(invocationMessage: InvocationMessage) {
@@ -122,6 +149,8 @@ export class HubConnection {
122149
if (requestedTransferMode === TransferMode.Binary && actualTransferMode === TransferMode.Text) {
123150
this.protocol = new Base64EncodedHubProtocol(this.protocol);
124151
}
152+
153+
this.configureTimeout();
125154
}
126155

127156
stop(): void {

client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IConnection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export interface IConnection {
99

1010
start(): Promise<void>;
1111
send(data: any): Promise<void>;
12-
stop(): void;
12+
stop(error?: Error): void;
1313

1414
onreceive: DataReceived;
1515
onclose: ConnectionClosed;
16-
}
16+
}

client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHubConnectionOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import { ILogger, LogLevel } from "./ILogger"
77

88
export interface IHubConnectionOptions extends IHttpConnectionOptions {
99
protocol?: IHubProtocol;
10+
serverTimeoutInMilliseconds?: number;
1011
}

client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DataReceived, TransportClosed } from "./Common"
55
import { IHttpClient } from "./HttpClient"
66
import { HttpError } from "./HttpError"
77
import { ILogger, LogLevel } from "./ILogger"
8+
import { IConnection } from "./IConnection"
89

910
export enum TransportType {
1011
WebSockets,
@@ -18,7 +19,7 @@ export const enum TransferMode {
1819
}
1920

2021
export interface ITransport {
21-
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode>;
22+
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode>;
2223
send(data: any): Promise<void>;
2324
stop(): void;
2425
onreceive: DataReceived;
@@ -35,7 +36,7 @@ export class WebSocketTransport implements ITransport {
3536
this.jwtBearer = jwtBearer;
3637
}
3738

38-
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> {
39+
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
3940

4041
return new Promise<TransferMode>((resolve, reject) => {
4142
url = url.replace(/^http/, "ws");
@@ -113,7 +114,7 @@ export class ServerSentEventsTransport implements ITransport {
113114
this.logger = logger;
114115
}
115116

116-
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> {
117+
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
117118
if (typeof (EventSource) === "undefined") {
118119
Promise.reject("EventSource not supported by the browser.");
119120
}
@@ -194,10 +195,13 @@ export class LongPollingTransport implements ITransport {
194195
this.logger = logger;
195196
}
196197

197-
connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> {
198+
connect(url: string, requestedTransferMode: TransferMode, connection: IConnection): Promise<TransferMode> {
198199
this.url = url;
199200
this.shouldPoll = true;
200201

202+
// Set a flag indicating we have inherent keep-alive in this transport.
203+
connection.features.inherentKeepAlive = true;
204+
201205
if (requestedTransferMode === TransferMode.Binary && (typeof new XMLHttpRequest().responseType !== "string")) {
202206
// This will work if we fix: https://github.com/aspnet/SignalR/issues/742
203207
throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");
@@ -300,4 +304,4 @@ async function send(httpClient: IHttpClient, url: string, jwtBearer: () => strin
300304
}
301305

302306
await httpClient.post(url, data, headers);
303-
}
307+
}

0 commit comments

Comments
 (0)