From d9b32262f89fe397e34c071d0f2ded2b91909766 Mon Sep 17 00:00:00 2001 From: Johan Vosloo Date: Sat, 14 Feb 2026 20:51:54 +0200 Subject: [PATCH] Fix eventsource reconnection: proper backoff and close() behavior Fixes three bugs in the EventSource plugin reconnection logic: 1. XOR instead of exponentiation: 2 ^ retryCount uses JavaScript bitwise XOR, not exponentiation. This produces erratic delays (e.g. 2 ^ 2 = 0, giving 0ms delay). Fixed to Math.pow(2, n). 2. Backoff only triggers on CLOSED state: The error handler only reconnected when readyState == EventSource.CLOSED, but browsers keep readyState = CONNECTING on connection-refused errors and auto-retry every ~3 seconds -- bypassing the backoff entirely. Fixed by closing the EventSource on error to stop the browser native auto-reconnect, then managing retry with proper backoff. 3. close() does not stop reconnection: Calling close() in a user on error handler sets readyState to CLOSED, which immediately triggers the plugin own error handler to call open() -- making the problem worse. Fixed by tracking a closed flag that is set in close() and checked before scheduling reconnection. Fixes #259 --- src/eventsource.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/eventsource.js b/src/eventsource.js index 75217794e..eb51e3bed 100644 --- a/src/eventsource.js +++ b/src/eventsource.js @@ -47,6 +47,8 @@ eventSource: null, listeners: [], retryCount: 0, + closed: false, + reconnectTimeout: null, open: function (url) { // calculate default values for URL argument. if (url == undefined) { @@ -68,6 +70,9 @@ } } + // Mark as not explicitly closed (allow reconnection) + stub.closed = false; + // Open the EventSource and get ready to populate event handlers stub.eventSource = new EventSource(url, { withCredentials: withCredentials, @@ -78,13 +83,17 @@ stub.retryCount = 0; }); - // On connection error, use exponential backoff to retry (random values from 1 second to 2^7 (128) seconds + // On connection error, use exponential backoff to retry stub.eventSource.addEventListener("error", function (event) { - // If the EventSource is closed, then try to reopen - if (stub.eventSource.readyState == EventSource.CLOSED) { + // Close the EventSource to prevent the browser's native auto-reconnect, + // which does not use backoff and can cause rapid-fire retries. + stub.eventSource.close(); + + // Only reconnect if the user has not explicitly called close() + if (!stub.closed) { stub.retryCount = Math.min(7, stub.retryCount + 1); - var timeout = Math.random() * (2 ^ stub.retryCount) * 500; - window.setTimeout(stub.open, timeout); + var timeout = Math.random() * Math.pow(2, stub.retryCount) * 500; + stub.reconnectTimeout = window.setTimeout(stub.open, timeout); } }); @@ -95,6 +104,11 @@ } }, close: function () { + stub.closed = true; + if (stub.reconnectTimeout != null) { + window.clearTimeout(stub.reconnectTimeout); + stub.reconnectTimeout = null; + } if (stub.eventSource != undefined) { stub.eventSource.close(); }