Unify C# and TS SignalR client auto-reconnect APIs (#10678)
This commit is contained in:
parent
915cc74df8
commit
81f2d46660
|
|
@ -1,20 +1,20 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
import { IReconnectPolicy } from "./IReconnectPolicy";
|
import { IRetryPolicy, RetryContext } from "./IRetryPolicy";
|
||||||
|
|
||||||
// 0, 2, 10, 30 second delays before reconnect attempts.
|
// 0, 2, 10, 30 second delays before reconnect attempts.
|
||||||
const DEFAULT_RETRY_DELAYS_IN_MILLISECONDS = [0, 2000, 10000, 30000, null];
|
const DEFAULT_RETRY_DELAYS_IN_MILLISECONDS = [0, 2000, 10000, 30000, null];
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
export class DefaultReconnectPolicy implements IReconnectPolicy {
|
export class DefaultReconnectPolicy implements IRetryPolicy {
|
||||||
private readonly retryDelays: Array<number | null>;
|
private readonly retryDelays: Array<number | null>;
|
||||||
|
|
||||||
constructor(retryDelays?: number[]) {
|
constructor(retryDelays?: number[]) {
|
||||||
this.retryDelays = retryDelays !== undefined ? [...retryDelays, null] : DEFAULT_RETRY_DELAYS_IN_MILLISECONDS;
|
this.retryDelays = retryDelays !== undefined ? [...retryDelays, null] : DEFAULT_RETRY_DELAYS_IN_MILLISECONDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
public nextRetryDelayInMilliseconds(previousRetryCount: number): number | null {
|
public nextRetryDelayInMilliseconds(retryContext: RetryContext): number | null {
|
||||||
return this.retryDelays[previousRetryCount];
|
return this.retryDelays[retryContext.previousRetryCount];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { HandshakeProtocol, HandshakeRequestMessage, HandshakeResponseMessage }
|
||||||
import { IConnection } from "./IConnection";
|
import { IConnection } from "./IConnection";
|
||||||
import { CancelInvocationMessage, CompletionMessage, IHubProtocol, InvocationMessage, MessageType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
|
import { CancelInvocationMessage, CompletionMessage, IHubProtocol, InvocationMessage, MessageType, StreamInvocationMessage, StreamItemMessage } from "./IHubProtocol";
|
||||||
import { ILogger, LogLevel } from "./ILogger";
|
import { ILogger, LogLevel } from "./ILogger";
|
||||||
import { IReconnectPolicy } from "./IReconnectPolicy";
|
import { IRetryPolicy } from "./IRetryPolicy";
|
||||||
import { IStreamResult } from "./Stream";
|
import { IStreamResult } from "./Stream";
|
||||||
import { Subject } from "./Subject";
|
import { Subject } from "./Subject";
|
||||||
import { Arg } from "./Utils";
|
import { Arg } from "./Utils";
|
||||||
|
|
@ -32,7 +32,7 @@ export class HubConnection {
|
||||||
private readonly cachedPingMessage: string | ArrayBuffer;
|
private readonly cachedPingMessage: string | ArrayBuffer;
|
||||||
private readonly connection: IConnection;
|
private readonly connection: IConnection;
|
||||||
private readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
private readonly reconnectPolicy?: IReconnectPolicy;
|
private readonly reconnectPolicy?: IRetryPolicy;
|
||||||
private protocol: IHubProtocol;
|
private protocol: IHubProtocol;
|
||||||
private handshakeProtocol: HandshakeProtocol;
|
private handshakeProtocol: HandshakeProtocol;
|
||||||
private callbacks: { [invocationId: string]: (invocationEvent: StreamItemMessage | CompletionMessage | null, error?: Error) => void };
|
private callbacks: { [invocationId: string]: (invocationEvent: StreamItemMessage | CompletionMessage | null, error?: Error) => void };
|
||||||
|
|
@ -81,11 +81,11 @@ export class HubConnection {
|
||||||
// create method that can be used by HubConnectionBuilder. An "internal" constructor would just
|
// create method that can be used by HubConnectionBuilder. An "internal" constructor would just
|
||||||
// be stripped away and the '.d.ts' file would have no constructor, which is interpreted as a
|
// be stripped away and the '.d.ts' file would have no constructor, which is interpreted as a
|
||||||
// public parameter-less constructor.
|
// public parameter-less constructor.
|
||||||
public static create(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IReconnectPolicy): HubConnection {
|
public static create(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IRetryPolicy): HubConnection {
|
||||||
return new HubConnection(connection, logger, protocol, reconnectPolicy);
|
return new HubConnection(connection, logger, protocol, reconnectPolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IReconnectPolicy) {
|
private constructor(connection: IConnection, logger: ILogger, protocol: IHubProtocol, reconnectPolicy?: IRetryPolicy) {
|
||||||
Arg.isRequired(connection, "connection");
|
Arg.isRequired(connection, "connection");
|
||||||
Arg.isRequired(logger, "logger");
|
Arg.isRequired(logger, "logger");
|
||||||
Arg.isRequired(protocol, "protocol");
|
Arg.isRequired(protocol, "protocol");
|
||||||
|
|
@ -666,11 +666,12 @@ export class HubConnection {
|
||||||
private async reconnect(error?: Error) {
|
private async reconnect(error?: Error) {
|
||||||
const reconnectStartTime = Date.now();
|
const reconnectStartTime = Date.now();
|
||||||
let previousReconnectAttempts = 0;
|
let previousReconnectAttempts = 0;
|
||||||
|
let retryError = error !== undefined ? error : new Error("Attempting to reconnect due to a unknown error.");
|
||||||
|
|
||||||
let nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, 0);
|
let nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, 0, retryError);
|
||||||
|
|
||||||
if (nextRetryDelay === null) {
|
if (nextRetryDelay === null) {
|
||||||
this.logger.log(LogLevel.Debug, "Connection not reconnecting because the IReconnectPolicy returned null on the first reconnect attempt.");
|
this.logger.log(LogLevel.Debug, "Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt.");
|
||||||
this.completeClose(error);
|
this.completeClose(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -732,9 +733,10 @@ export class HubConnection {
|
||||||
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect attempt. Done reconnecting.");
|
this.logger.log(LogLevel.Debug, "Connection left the reconnecting state during reconnect attempt. Done reconnecting.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime);
|
retryError = e instanceof Error ? e : new Error(e.toString());
|
||||||
|
nextRetryDelay = this.getNextRetryDelay(previousReconnectAttempts++, Date.now() - reconnectStartTime, retryError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(LogLevel.Information, `Reconnect retries have been exhausted after ${Date.now() - reconnectStartTime} ms and ${previousReconnectAttempts} failed attempts. Connection disconnecting.`);
|
this.logger.log(LogLevel.Information, `Reconnect retries have been exhausted after ${Date.now() - reconnectStartTime} ms and ${previousReconnectAttempts} failed attempts. Connection disconnecting.`);
|
||||||
|
|
@ -742,11 +744,15 @@ export class HubConnection {
|
||||||
this.completeClose();
|
this.completeClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNextRetryDelay(previousRetryCount: number, elapsedMilliseconds: number) {
|
private getNextRetryDelay(previousRetryCount: number, elapsedMilliseconds: number, retryReason: Error) {
|
||||||
try {
|
try {
|
||||||
return this.reconnectPolicy!.nextRetryDelayInMilliseconds(previousRetryCount, elapsedMilliseconds);
|
return this.reconnectPolicy!.nextRetryDelayInMilliseconds({
|
||||||
|
elapsedMilliseconds,
|
||||||
|
previousRetryCount,
|
||||||
|
retryReason,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.log(LogLevel.Error, `IReconnectPolicy.nextRetryDelayInMilliseconds(${previousRetryCount}, ${elapsedMilliseconds}) threw error '${e}'.`);
|
this.logger.log(LogLevel.Error, `IRetryPolicy.nextRetryDelayInMilliseconds(${previousRetryCount}, ${elapsedMilliseconds}) threw error '${e}'.`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { HubConnection } from "./HubConnection";
|
||||||
import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
|
import { IHttpConnectionOptions } from "./IHttpConnectionOptions";
|
||||||
import { IHubProtocol } from "./IHubProtocol";
|
import { IHubProtocol } from "./IHubProtocol";
|
||||||
import { ILogger, LogLevel } from "./ILogger";
|
import { ILogger, LogLevel } from "./ILogger";
|
||||||
import { IReconnectPolicy } from "./IReconnectPolicy";
|
import { IRetryPolicy } from "./IRetryPolicy";
|
||||||
import { HttpTransportType } from "./ITransport";
|
import { HttpTransportType } from "./ITransport";
|
||||||
import { JsonHubProtocol } from "./JsonHubProtocol";
|
import { JsonHubProtocol } from "./JsonHubProtocol";
|
||||||
import { NullLogger } from "./Loggers";
|
import { NullLogger } from "./Loggers";
|
||||||
|
|
@ -51,7 +51,7 @@ export class HubConnectionBuilder {
|
||||||
|
|
||||||
/** If defined, this indicates the client should automatically attempt to reconnect if the connection is lost. */
|
/** If defined, this indicates the client should automatically attempt to reconnect if the connection is lost. */
|
||||||
/** @internal */
|
/** @internal */
|
||||||
public reconnectPolicy?: IReconnectPolicy;
|
public reconnectPolicy?: IRetryPolicy;
|
||||||
|
|
||||||
/** Configures console logging for the {@link @aspnet/signalr.HubConnection}.
|
/** Configures console logging for the {@link @aspnet/signalr.HubConnection}.
|
||||||
*
|
*
|
||||||
|
|
@ -164,10 +164,10 @@ export class HubConnectionBuilder {
|
||||||
|
|
||||||
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost.
|
/** Configures the {@link @aspnet/signalr.HubConnection} to automatically attempt to reconnect if the connection is lost.
|
||||||
*
|
*
|
||||||
* @param {IReconnectPolicy} reconnectPolicy An {@link @aspnet/signalR.IReconnectPolicy} that controls the timing and number of reconnect attempts.
|
* @param {IRetryPolicy} reconnectPolicy An {@link @aspnet/signalR.IRetryPolicy} that controls the timing and number of reconnect attempts.
|
||||||
*/
|
*/
|
||||||
public withAutomaticReconnect(reconnectPolicy: IReconnectPolicy): HubConnectionBuilder;
|
public withAutomaticReconnect(reconnectPolicy: IRetryPolicy): HubConnectionBuilder;
|
||||||
public withAutomaticReconnect(retryDelaysOrReconnectPolicy?: number[] | IReconnectPolicy): HubConnectionBuilder {
|
public withAutomaticReconnect(retryDelaysOrReconnectPolicy?: number[] | IRetryPolicy): HubConnectionBuilder {
|
||||||
if (this.reconnectPolicy) {
|
if (this.reconnectPolicy) {
|
||||||
throw new Error("A reconnectPolicy has already been set.");
|
throw new Error("A reconnectPolicy has already been set.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
/** An abstraction that controls when the client attempts to reconnect and how many times it does so. */
|
|
||||||
export interface IReconnectPolicy {
|
|
||||||
/** Called after the transport loses the connection.
|
|
||||||
*
|
|
||||||
* @param {number} previousRetryCount The number of consecutive failed reconnect attempts so far.
|
|
||||||
*
|
|
||||||
* @param {number} elapsedMilliseconds The amount of time in milliseconds spent reconnecting so far.
|
|
||||||
*
|
|
||||||
* @returns {number | null} The amount of time in milliseconds to wait before the next reconnect attempt. `null` tells the client to stop retrying and close.
|
|
||||||
*/
|
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number, elapsedMilliseconds: number): number | null;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
/** An abstraction that controls when the client attempts to reconnect and how many times it does so. */
|
||||||
|
export interface IRetryPolicy {
|
||||||
|
/** Called after the transport loses the connection.
|
||||||
|
*
|
||||||
|
* @param {RetryContext} retryContext Details related to the retry event to help determine how long to wait for the next retry.
|
||||||
|
*
|
||||||
|
* @returns {number | null} The amount of time in milliseconds to wait before the next retry. `null` tells the client to stop retrying.
|
||||||
|
*/
|
||||||
|
nextRetryDelayInMilliseconds(retryContext: RetryContext): number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RetryContext {
|
||||||
|
/**
|
||||||
|
* The number of consecutive failed tries so far.
|
||||||
|
*/
|
||||||
|
readonly previousRetryCount: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of time in milliseconds spent retrying so far.
|
||||||
|
*/
|
||||||
|
readonly elapsedMilliseconds: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error that forced the upcoming retry.
|
||||||
|
*/
|
||||||
|
readonly retryReason: Error;
|
||||||
|
}
|
||||||
|
|
@ -21,4 +21,4 @@ export { IStreamSubscriber, IStreamResult, ISubscription } from "./Stream";
|
||||||
export { NullLogger } from "./Loggers";
|
export { NullLogger } from "./Loggers";
|
||||||
export { JsonHubProtocol } from "./JsonHubProtocol";
|
export { JsonHubProtocol } from "./JsonHubProtocol";
|
||||||
export { Subject } from "./Subject";
|
export { Subject } from "./Subject";
|
||||||
export { IReconnectPolicy } from "./IReconnectPolicy";
|
export { IRetryPolicy, RetryContext } from "./IRetryPolicy";
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { DefaultReconnectPolicy } from "../src/DefaultReconnectPolicy";
|
import { DefaultReconnectPolicy } from "../src/DefaultReconnectPolicy";
|
||||||
import { HubConnection, HubConnectionState } from "../src/HubConnection";
|
import { HubConnection, HubConnectionState } from "../src/HubConnection";
|
||||||
|
import { RetryContext } from "../src/IRetryPolicy";
|
||||||
import { JsonHubProtocol } from "../src/JsonHubProtocol";
|
import { JsonHubProtocol } from "../src/JsonHubProtocol";
|
||||||
|
|
||||||
import { VerifyLogger } from "./Common";
|
import { VerifyLogger } from "./Common";
|
||||||
|
|
@ -46,15 +47,17 @@ describe("auto reconnect", () => {
|
||||||
|
|
||||||
let lastRetryCount = -1;
|
let lastRetryCount = -1;
|
||||||
let lastElapsedMs = -1;
|
let lastElapsedMs = -1;
|
||||||
|
let retryReason = null;
|
||||||
let onreconnectingCount = 0;
|
let onreconnectingCount = 0;
|
||||||
let onreconnectedCount = 0;
|
let onreconnectedCount = 0;
|
||||||
let closeCount = 0;
|
let closeCount = 0;
|
||||||
|
|
||||||
const connection = new TestConnection();
|
const connection = new TestConnection();
|
||||||
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number, elapsedMilliseconds: number) {
|
nextRetryDelayInMilliseconds(retryContext: RetryContext) {
|
||||||
lastRetryCount = previousRetryCount;
|
lastRetryCount = retryContext.previousRetryCount;
|
||||||
lastElapsedMs = elapsedMilliseconds;
|
lastElapsedMs = retryContext.elapsedMilliseconds;
|
||||||
|
retryReason = retryContext.retryReason;
|
||||||
nextRetryDelayCalledPromise.resolve();
|
nextRetryDelayCalledPromise.resolve();
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
@ -81,8 +84,11 @@ describe("auto reconnect", () => {
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const oncloseError = new Error("Connection lost");
|
||||||
|
const continueRetryingError = new Error("Reconnect attempt failed");
|
||||||
|
|
||||||
// Typically this would be called by the transport
|
// Typically this would be called by the transport
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
nextRetryDelayCalledPromise = new PromiseSource();
|
nextRetryDelayCalledPromise = new PromiseSource();
|
||||||
|
|
@ -90,17 +96,19 @@ describe("auto reconnect", () => {
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(0);
|
expect(lastRetryCount).toBe(0);
|
||||||
expect(lastElapsedMs).toBe(0);
|
expect(lastElapsedMs).toBe(0);
|
||||||
|
expect(retryReason).toBe(oncloseError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
||||||
// Make sure the the Promise is "handled" immediately upon rejection or else this test fails.
|
// Make sure the the Promise is "handled" immediately upon rejection or else this test fails.
|
||||||
continueRetryingPromise.catch(() => { });
|
continueRetryingPromise.catch(() => { });
|
||||||
continueRetryingPromise.reject(new Error("Reconnect attempt failed"));
|
continueRetryingPromise.reject(continueRetryingError);
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
|
|
||||||
expect(lastRetryCount).toBe(1);
|
expect(lastRetryCount).toBe(1);
|
||||||
expect(lastElapsedMs).toBeGreaterThanOrEqual(0);
|
expect(lastElapsedMs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(retryReason).toBe(continueRetryingError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -133,18 +141,20 @@ describe("auto reconnect", () => {
|
||||||
|
|
||||||
let lastRetryCount = -1;
|
let lastRetryCount = -1;
|
||||||
let lastElapsedMs = -1;
|
let lastElapsedMs = -1;
|
||||||
|
let retryReason = null;
|
||||||
let onreconnectingCount = 0;
|
let onreconnectingCount = 0;
|
||||||
let onreconnectedCount = 0;
|
let onreconnectedCount = 0;
|
||||||
let closeCount = 0;
|
let closeCount = 0;
|
||||||
|
|
||||||
const connection = new TestConnection();
|
const connection = new TestConnection();
|
||||||
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number, elapsedMilliseconds: number) {
|
nextRetryDelayInMilliseconds(retryContext: RetryContext) {
|
||||||
lastRetryCount = previousRetryCount;
|
lastRetryCount = retryContext.previousRetryCount;
|
||||||
lastElapsedMs = elapsedMilliseconds;
|
lastElapsedMs = retryContext.elapsedMilliseconds;
|
||||||
|
retryReason = retryContext.retryReason;
|
||||||
nextRetryDelayCalledPromise.resolve();
|
nextRetryDelayCalledPromise.resolve();
|
||||||
|
|
||||||
return previousRetryCount === 0 ? 0 : null;
|
return retryContext.previousRetryCount === 0 ? 0 : null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -163,12 +173,15 @@ describe("auto reconnect", () => {
|
||||||
|
|
||||||
await hubConnection.start();
|
await hubConnection.start();
|
||||||
|
|
||||||
|
const oncloseError = new Error("Connection lost");
|
||||||
|
const startError = new Error("Reconnect attempt failed");
|
||||||
|
|
||||||
connection.start = () => {
|
connection.start = () => {
|
||||||
return Promise.reject("Reconnect attempt failed");
|
throw startError;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typically this would be called by the transport
|
// Typically this would be called by the transport
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
nextRetryDelayCalledPromise = new PromiseSource();
|
nextRetryDelayCalledPromise = new PromiseSource();
|
||||||
|
|
@ -176,6 +189,7 @@ describe("auto reconnect", () => {
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(0);
|
expect(lastRetryCount).toBe(0);
|
||||||
expect(lastElapsedMs).toBe(0);
|
expect(lastElapsedMs).toBe(0);
|
||||||
|
expect(retryReason).toBe(oncloseError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -185,6 +199,7 @@ describe("auto reconnect", () => {
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Disconnected);
|
expect(hubConnection.state).toBe(HubConnectionState.Disconnected);
|
||||||
expect(lastRetryCount).toBe(1);
|
expect(lastRetryCount).toBe(1);
|
||||||
expect(lastElapsedMs).toBeGreaterThanOrEqual(0);
|
expect(lastElapsedMs).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(retryReason).toBe(startError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(1);
|
expect(closeCount).toBe(1);
|
||||||
|
|
@ -198,15 +213,17 @@ describe("auto reconnect", () => {
|
||||||
|
|
||||||
let lastRetryCount = -1;
|
let lastRetryCount = -1;
|
||||||
let lastElapsedMs = -1;
|
let lastElapsedMs = -1;
|
||||||
|
let retryReason = null;
|
||||||
let onreconnectingCount = 0;
|
let onreconnectingCount = 0;
|
||||||
let onreconnectedCount = 0;
|
let onreconnectedCount = 0;
|
||||||
let closeCount = 0;
|
let closeCount = 0;
|
||||||
|
|
||||||
const connection = new TestConnection();
|
const connection = new TestConnection();
|
||||||
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number, elapsedMilliseconds: number) {
|
nextRetryDelayInMilliseconds(retryContext: RetryContext) {
|
||||||
lastRetryCount = previousRetryCount;
|
lastRetryCount = retryContext.previousRetryCount;
|
||||||
lastElapsedMs = elapsedMilliseconds;
|
lastElapsedMs = retryContext.elapsedMilliseconds;
|
||||||
|
retryReason = retryContext.retryReason;
|
||||||
nextRetryDelayCalledPromise.resolve();
|
nextRetryDelayCalledPromise.resolve();
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
@ -227,8 +244,11 @@ describe("auto reconnect", () => {
|
||||||
|
|
||||||
await hubConnection.start();
|
await hubConnection.start();
|
||||||
|
|
||||||
|
const oncloseError = new Error("Connection lost 1");
|
||||||
|
const oncloseError2 = new Error("Connection lost 2");
|
||||||
|
|
||||||
// Typically this would be called by the transport
|
// Typically this would be called by the transport
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
nextRetryDelayCalledPromise = new PromiseSource();
|
nextRetryDelayCalledPromise = new PromiseSource();
|
||||||
|
|
@ -236,6 +256,7 @@ describe("auto reconnect", () => {
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(0);
|
expect(lastRetryCount).toBe(0);
|
||||||
expect(lastElapsedMs).toBe(0);
|
expect(lastElapsedMs).toBe(0);
|
||||||
|
expect(retryReason).toBe(oncloseError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -250,13 +271,14 @@ describe("auto reconnect", () => {
|
||||||
expect(onreconnectedCount).toBe(1);
|
expect(onreconnectedCount).toBe(1);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError2);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
|
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(0);
|
expect(lastRetryCount).toBe(0);
|
||||||
expect(lastElapsedMs).toBe(0);
|
expect(lastElapsedMs).toBe(0);
|
||||||
|
expect(retryReason).toBe(oncloseError2);
|
||||||
expect(onreconnectingCount).toBe(2);
|
expect(onreconnectingCount).toBe(2);
|
||||||
expect(onreconnectedCount).toBe(1);
|
expect(onreconnectedCount).toBe(1);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -362,6 +384,7 @@ describe("auto reconnect", () => {
|
||||||
let nextRetryDelayCalledPromise = new PromiseSource();
|
let nextRetryDelayCalledPromise = new PromiseSource();
|
||||||
|
|
||||||
let lastRetryCount = 0;
|
let lastRetryCount = 0;
|
||||||
|
let retryReason = null;
|
||||||
let onreconnectingCount = 0;
|
let onreconnectingCount = 0;
|
||||||
let onreconnectedCount = 0;
|
let onreconnectedCount = 0;
|
||||||
let closeCount = 0;
|
let closeCount = 0;
|
||||||
|
|
@ -369,8 +392,9 @@ describe("auto reconnect", () => {
|
||||||
// Disable autoHandshake in TestConnection
|
// Disable autoHandshake in TestConnection
|
||||||
const connection = new TestConnection(false);
|
const connection = new TestConnection(false);
|
||||||
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number) {
|
nextRetryDelayInMilliseconds(retryContext: RetryContext) {
|
||||||
lastRetryCount = previousRetryCount;
|
lastRetryCount = retryContext.previousRetryCount;
|
||||||
|
retryReason = retryContext.retryReason;
|
||||||
nextRetryDelayCalledPromise.resolve();
|
nextRetryDelayCalledPromise.resolve();
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
@ -400,14 +424,18 @@ describe("auto reconnect", () => {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const oncloseError = new Error("Connection lost 1");
|
||||||
|
const oncloseError2 = new Error("Connection lost 2");
|
||||||
|
|
||||||
// Typically this would be called by the transport
|
// Typically this would be called by the transport
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
nextRetryDelayCalledPromise = new PromiseSource();
|
nextRetryDelayCalledPromise = new PromiseSource();
|
||||||
|
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(0);
|
expect(lastRetryCount).toBe(0);
|
||||||
|
expect(retryReason).toBe(oncloseError);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -416,12 +444,13 @@ describe("auto reconnect", () => {
|
||||||
replacedStartCalledPromise = new PromiseSource();
|
replacedStartCalledPromise = new PromiseSource();
|
||||||
|
|
||||||
// Fail underlying connection during reconnect during handshake
|
// Fail underlying connection during reconnect during handshake
|
||||||
connection.onclose!(new Error("Connection lost"));
|
connection.onclose!(oncloseError2);
|
||||||
|
|
||||||
await nextRetryDelayCalledPromise;
|
await nextRetryDelayCalledPromise;
|
||||||
|
|
||||||
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
expect(hubConnection.state).toBe(HubConnectionState.Reconnecting);
|
||||||
expect(lastRetryCount).toBe(1);
|
expect(lastRetryCount).toBe(1);
|
||||||
|
expect(retryReason).toBe(oncloseError2);
|
||||||
expect(onreconnectingCount).toBe(1);
|
expect(onreconnectingCount).toBe(1);
|
||||||
expect(onreconnectedCount).toBe(0);
|
expect(onreconnectedCount).toBe(0);
|
||||||
expect(closeCount).toBe(0);
|
expect(closeCount).toBe(0);
|
||||||
|
|
@ -461,8 +490,8 @@ describe("auto reconnect", () => {
|
||||||
// Disable autoHandshake in TestConnection
|
// Disable autoHandshake in TestConnection
|
||||||
const connection = new TestConnection(false);
|
const connection = new TestConnection(false);
|
||||||
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
const hubConnection = HubConnection.create(connection, logger, new JsonHubProtocol(), {
|
||||||
nextRetryDelayInMilliseconds(previousRetryCount: number) {
|
nextRetryDelayInMilliseconds(retryContext: RetryContext) {
|
||||||
lastRetryCount = previousRetryCount;
|
lastRetryCount = retryContext.previousRetryCount;
|
||||||
nextRetryDelayCalledPromise.resolve();
|
nextRetryDelayCalledPromise.resolve();
|
||||||
return 0;
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,13 @@ describe("HubConnectionBuilder", () => {
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
for (const delay of DEFAULT_RETRY_DELAYS_IN_MILLISECONDS) {
|
for (const delay of DEFAULT_RETRY_DELAYS_IN_MILLISECONDS) {
|
||||||
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryCount++, 0)).toBe(delay);
|
const retryContext = {
|
||||||
|
previousRetryCount: retryCount++,
|
||||||
|
elapsedMilliseconds: 0,
|
||||||
|
retryReason: new Error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -333,23 +339,47 @@ describe("HubConnectionBuilder", () => {
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
for (const delay of customRetryDelays) {
|
for (const delay of customRetryDelays) {
|
||||||
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryCount++, 0)).toBe(delay);
|
const retryContext = {
|
||||||
|
previousRetryCount: retryCount++,
|
||||||
|
elapsedMilliseconds: 0,
|
||||||
|
retryReason: new Error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryCount, 0)).toBe(null);
|
const retryContextFinal = {
|
||||||
|
previousRetryCount: retryCount++,
|
||||||
|
elapsedMilliseconds: 0,
|
||||||
|
retryReason: new Error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContextFinal)).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("withAutomaticReconnect uses a custom IReconnectPolicy when provided", () => {
|
it("withAutomaticReconnect uses a custom IRetryPolicy when provided", () => {
|
||||||
const customRetryDelays = [127, 0, 0, 1];
|
const customRetryDelays = [127, 0, 0, 1];
|
||||||
const builder = new HubConnectionBuilder()
|
const builder = new HubConnectionBuilder()
|
||||||
.withAutomaticReconnect(new DefaultReconnectPolicy(customRetryDelays));
|
.withAutomaticReconnect(new DefaultReconnectPolicy(customRetryDelays));
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
for (const delay of customRetryDelays) {
|
for (const delay of customRetryDelays) {
|
||||||
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryCount++, 0)).toBe(delay);
|
const retryContext = {
|
||||||
|
previousRetryCount: retryCount++,
|
||||||
|
elapsedMilliseconds: 0,
|
||||||
|
retryReason: new Error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContext)).toBe(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryCount, 0)).toBe(null);
|
const retryContextFinal = {
|
||||||
|
previousRetryCount: retryCount++,
|
||||||
|
elapsedMilliseconds: 0,
|
||||||
|
retryReason: new Error(),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(builder.reconnectPolicy!.nextRetryDelayInMilliseconds(retryContextFinal)).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue