diff --git a/lib/client.js b/lib/client.js index 31252a2..b401880 100644 --- a/lib/client.js +++ b/lib/client.js @@ -25,7 +25,8 @@ const permanentCaps = [ "soju.im/bouncer-networks", ]; -const RECONNECT_DELAY_SEC = 10; +const RECONNECT_MIN_DELAY_MSEC = 10 * 1000; // 10s +const RECONNECT_MAX_DELAY_MSEC = 10 * 60 * 1000; // 10min // WebSocket status codes // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 @@ -64,6 +65,38 @@ class IRCError extends Error { } } +/** + * Implements a simple exponential backoff. + */ +class Backoff { + n = 0; + + constructor(min, max) { + this.min = min; + this.max = max; + } + + reset() { + this.n = 0; + } + + next() { + if (this.n === 0) { + this.n = 1; + return this.min; + } + + let dur = this.n * this.min; + if (dur > this.max) { + dur = this.max; + } else { + this.n *= 2; + } + + return dur; + } +} + export default class Client extends EventTarget { static Status = { DISCONNECTED: "disconnected", @@ -95,6 +128,7 @@ export default class Client extends EventTarget { batches = new Map(); autoReconnect = true; reconnectTimeoutID = null; + reconnectBackoff = new Backoff(RECONNECT_MIN_DELAY_MSEC, RECONNECT_MAX_DELAY_MSEC); pingIntervalID = null; pendingCmds = { WHO: Promise.resolve(null), @@ -171,11 +205,12 @@ export default class Client extends EventTarget { }; window.addEventListener("online", handleOnline); } else { - console.info("Reconnecting to server in " + RECONNECT_DELAY_SEC + " seconds"); + let delay = this.reconnectBackoff.next(); + console.info("Reconnecting to server in " + (delay / 1000) + " seconds"); clearTimeout(this.reconnectTimeoutID); this.reconnectTimeoutID = setTimeout(() => { this.reconnect(); - }, RECONNECT_DELAY_SEC * 1000); + }, delay); } } }); @@ -210,6 +245,8 @@ export default class Client extends EventTarget { console.log("Connection opened"); this.setStatus(Client.Status.REGISTERING); + this.reconnectBackoff.reset(); + this.nick = this.params.nick; this.send({ command: "CAP", params: ["LS", "302"] });