-
-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #58 from AlCalzone/reconnection
Add experimental support for automatic reconnection
- Loading branch information
Showing
10 changed files
with
1,062 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/// <reference types="node" /> | ||
import { EventEmitter } from "events"; | ||
import { TradfriClient } from ".."; | ||
/** Configures options for connection watching and automatic reconnection */ | ||
export interface ConnectionWatcherOptions { | ||
/** The interval in ms between consecutive pings */ | ||
pingInterval: number; | ||
/** How many pings have to consecutively fail until the gateway is assumed offline */ | ||
failedPingCountUntilOffline: number; | ||
/** | ||
* How much the interval between consecutive pings should be increased while the gateway is offline. | ||
* The actual interval is calculated by <ping interval> * <backoff factor> ** <min(5, # offline pings)> | ||
*/ | ||
failedPingBackoffFactor: number; | ||
/** Whether automatic reconnection is enabled */ | ||
reconnectionEnabled: boolean; | ||
/** How many pings have to consecutively fail while the gateway is offline until a reconnection is triggered */ | ||
offlinePingCountUntilReconnect: number; | ||
/** After how many failed reconnects we give up. Number.POSITIVE_INFINITY to never gonna give you up, never gonna let you down... */ | ||
maximumReconnects: number; | ||
} | ||
export declare type ConnectionEvents = "ping succeeded" | "ping failed" | "connection alive" | "connection lost" | "gateway offline" | "reconnecting" | "give up"; | ||
export declare type PingFailedCallback = (failedPingCount: number) => void; | ||
export declare type ReconnectingCallback = (reconnectAttempt: number, maximumReconnects: number) => void; | ||
export interface ConnectionWatcher { | ||
on(event: "ping succeeded", callback: () => void): this; | ||
on(event: "ping failed", callback: PingFailedCallback): this; | ||
on(event: "connection alive", callback: () => void): this; | ||
on(event: "connection lost", callback: () => void): this; | ||
on(event: "gateway offline", callback: () => void): this; | ||
on(event: "reconnecting", callback: ReconnectingCallback): this; | ||
on(event: "give up", callback: () => void): this; | ||
on(event: ConnectionEvents, callback: (...args: any[]) => void): this; | ||
once(event: "ping succeeded", callback: () => void): this; | ||
once(event: "ping failed", callback: PingFailedCallback): this; | ||
once(event: "connection alive", callback: () => void): this; | ||
once(event: "connection lost", callback: () => void): this; | ||
once(event: "gateway offline", callback: () => void): this; | ||
once(event: "reconnecting", callback: ReconnectingCallback): this; | ||
once(event: "give up", callback: () => void): this; | ||
once(event: ConnectionEvents, callback: (...args: any[]) => void): this; | ||
removeListener(event: "ping succeeded", callback: () => void): this; | ||
removeListener(event: "ping failed", callback: PingFailedCallback): this; | ||
removeListener(event: "connection alive", callback: () => void): this; | ||
removeListener(event: "connection lost", callback: () => void): this; | ||
removeListener(event: "gateway offline", callback: () => void): this; | ||
removeListener(event: "reconnecting", callback: ReconnectingCallback): this; | ||
removeListener(event: "give up", callback: () => void): this; | ||
removeAllListeners(event?: ConnectionEvents): this; | ||
} | ||
/** | ||
* Watches the connection of a TradfriClient and notifies about changes in the connection state | ||
*/ | ||
export declare class ConnectionWatcher extends EventEmitter { | ||
private client; | ||
constructor(client: TradfriClient, options?: Partial<ConnectionWatcherOptions>); | ||
private options; | ||
private pingTimer; | ||
/** Starts watching the connection */ | ||
start(): void; | ||
private isActive; | ||
/** Stops watching the connection */ | ||
stop(): void; | ||
private connectionAlive; | ||
private failedPingCount; | ||
private offlinePingCount; | ||
private resetAttempts; | ||
private pingThread(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
"use strict"; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const events_1 = require("events"); | ||
const logger_1 = require("./logger"); | ||
const defaultOptions = Object.freeze({ | ||
pingInterval: 10000, | ||
failedPingCountUntilOffline: 1, | ||
failedPingBackoffFactor: 1.5, | ||
reconnectionEnabled: true, | ||
offlinePingCountUntilReconnect: 3, | ||
maximumReconnects: Number.POSITIVE_INFINITY, | ||
}); | ||
function checkOptions(opts) { | ||
if (opts.pingInterval != null && (opts.pingInterval < 1000 || opts.pingInterval > 5 * 60000)) { | ||
throw new Error("The ping interval must be between 1s and 5 minutes"); | ||
} | ||
if (opts.failedPingCountUntilOffline != null && (opts.failedPingCountUntilOffline < 1 || opts.failedPingCountUntilOffline > 10)) { | ||
throw new Error("The failed ping count to assume the gateway as offline must be between 1 and 10"); | ||
} | ||
if (opts.failedPingBackoffFactor != null && (opts.failedPingBackoffFactor < 1 || opts.failedPingBackoffFactor > 3)) { | ||
throw new Error("The interval back-off factor for failed pings must be between 1 and 3"); | ||
} | ||
if (opts.offlinePingCountUntilReconnect != null && (opts.offlinePingCountUntilReconnect < 1 || opts.offlinePingCountUntilReconnect > 10)) { | ||
throw new Error("The failed ping count before a reconnect attempt must be between 1 and 10"); | ||
} | ||
if (opts.maximumReconnects != null && opts.maximumReconnects < 1) { | ||
throw new Error("The maximum number of reconnect attempts must be positive"); | ||
} | ||
} | ||
// tslint:enable:unified-signatures | ||
/** | ||
* Watches the connection of a TradfriClient and notifies about changes in the connection state | ||
*/ | ||
class ConnectionWatcher extends events_1.EventEmitter { | ||
constructor(client, options) { | ||
super(); | ||
this.client = client; | ||
this.failedPingCount = 0; | ||
this.offlinePingCount = 0; | ||
this.resetAttempts = 0; | ||
if (options == null) | ||
options = {}; | ||
checkOptions(options); | ||
this.options = Object.assign({}, defaultOptions, options); | ||
} | ||
/** Starts watching the connection */ | ||
start() { | ||
if (this.pingTimer != null) | ||
throw new Error("The connection watcher is already running"); | ||
this.isActive = true; | ||
this.pingTimer = setTimeout(() => this.pingThread(), this.options.pingInterval); | ||
} | ||
/** Stops watching the connection */ | ||
stop() { | ||
if (this.pingTimer != null) { | ||
clearTimeout(this.pingTimer); | ||
this.pingTimer = null; | ||
} | ||
this.isActive = false; | ||
} | ||
pingThread() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const oldValue = this.connectionAlive; | ||
this.connectionAlive = yield this.client.ping(); | ||
// see if the connection state has changed | ||
if (this.connectionAlive) { | ||
logger_1.log("ping succeeded", "debug"); | ||
this.emit("ping succeeded"); | ||
// connection is now alive again | ||
if (oldValue === false) { | ||
logger_1.log(`The connection is alive again after ${this.failedPingCount} failed pings`, "debug"); | ||
this.emit("connection alive"); | ||
} | ||
// reset all counters because the connection is good again | ||
this.failedPingCount = 0; | ||
this.offlinePingCount = 0; | ||
this.resetAttempts = 0; | ||
} | ||
else { | ||
this.failedPingCount++; | ||
logger_1.log(`ping failed (#${this.failedPingCount})`, "debug"); | ||
this.emit("ping failed", this.failedPingCount); | ||
if (oldValue === true) { | ||
logger_1.log("The connection was lost", "debug"); | ||
this.emit("connection lost"); | ||
} | ||
// connection is dead | ||
if (this.failedPingCount >= this.options.failedPingCountUntilOffline) { | ||
if (this.failedPingCount === this.options.failedPingCountUntilOffline) { | ||
// we just reached the threshold, say the gateway is offline | ||
logger_1.log(`${this.failedPingCount} consecutive pings failed. The gateway is offline.`, "debug"); | ||
this.emit("gateway offline"); | ||
} | ||
// if we should reconnect automatically, count the offline pings | ||
if (this.options.reconnectionEnabled) { | ||
this.offlinePingCount++; | ||
// as soon as we pass the threshold, reset the client | ||
if (this.offlinePingCount >= this.options.offlinePingCountUntilReconnect) { | ||
if (this.resetAttempts < this.options.maximumReconnects) { | ||
// trigger a reconnect | ||
this.offlinePingCount = 0; | ||
this.resetAttempts++; | ||
logger_1.log(`Trying to reconnect... Attempt ${this.resetAttempts} of ${this.options.maximumReconnects === Number.POSITIVE_INFINITY ? "∞" : this.options.maximumReconnects}`, "debug"); | ||
this.emit("reconnecting", this.resetAttempts, this.options.maximumReconnects); | ||
this.client.reset(); | ||
} | ||
else if (this.resetAttempts === this.options.maximumReconnects) { | ||
// don't try anymore | ||
logger_1.log("Maximum reconnect attempts reached... giving up.", "debug"); | ||
this.emit("give up"); | ||
// increase the counter once more so this branch doesn't get hit | ||
this.resetAttempts++; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// schedule the next ping | ||
if (this.isActive) { | ||
const nextTimeout = Math.round(this.options.pingInterval * Math.pow(this.options.failedPingBackoffFactor, Math.min(5, this.failedPingCount))); | ||
logger_1.log("setting next timeout in " + nextTimeout, "debug"); | ||
this.pingTimer = setTimeout(() => this.pingThread(), nextTimeout); | ||
} | ||
}); | ||
} | ||
} | ||
exports.ConnectionWatcher = ConnectionWatcher; |
Oops, something went wrong.