diff --git a/README.md b/README.md index 5e7a7b0..6bdaa2d 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,10 @@ wsOpts = { awareness: new awarenessProtocol.Awareness(ydoc), // Specify the maximum amount to wait between reconnects (we use exponential backoff). maxBackoffTime: 2500 + // Specify the maximum amount to wait between reconnects if previous connection succeeded (we use exponential backoff). + // This prevents connections that close right after their successful creation from being retried immediately. + // Backoff only resets when a connection has been open for `maxBackoffIntervalOnSuccessfulConnects * 2` ms + maxBackoffIntervalOnSuccessfulConnects: 2500 } ``` diff --git a/src/y-websocket.js b/src/y-websocket.js index 69ae05f..be8fb7e 100644 --- a/src/y-websocket.js +++ b/src/y-websocket.js @@ -172,6 +172,16 @@ const closeWebsocketConnection = (provider, ws, event) => { * @param {WebsocketProvider} provider */ const setupWS = (provider) => { + // Start with 100ms interval between successful connects and increase with exponential backoff + const backoff = math.min(math.pow(2, provider.wsSubsequentConnects) * 50, provider.maxBackoffIntervalOnSuccessfulConnects) + const timeout = provider.wsLastConnectAt ? Date.now() - provider.wsLastConnectAt.getTime() : backoff + if (timeout < backoff) { + setTimeout(setupWS, backoff - timeout, provider) + return + } else if (timeout > provider.maxBackoffIntervalOnSuccessfulConnects * 2) { + provider.wsSubsequentConnects = 0 + } + if (provider.shouldConnect && provider.ws === null) { const websocket = new provider._WS(provider.url, provider.protocols) websocket.binaryType = 'arraybuffer' @@ -194,6 +204,8 @@ const setupWS = (provider) => { closeWebsocketConnection(provider, websocket, event) } websocket.onopen = () => { + provider.wsLastConnectAt = new Date() + provider.wsSubsequentConnects++ provider.wsLastMessageReceived = time.getUnixTime() provider.wsconnecting = false provider.wsconnected = true @@ -265,6 +277,7 @@ export class WebsocketProvider extends ObservableV2 { * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds * @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff) + * @param {number} [opts.maxBackoffIntervalOnSuccessfulConnects] Maximum amount of time to wait before trying to reconnect if previous connection succeeded (we try to reconnect using exponential backoff). Backoff only resets after a connection has been open for `maxBackoffIntervalOnSuccessfulConnects * 2` ms * @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication */ constructor (serverUrl, roomname, doc, { @@ -275,6 +288,7 @@ export class WebsocketProvider extends ObservableV2 { WebSocketPolyfill = WebSocket, resyncInterval = -1, maxBackoffTime = 2500, + maxBackoffIntervalOnSuccessfulConnects = 2500, disableBc = false } = {}) { super() @@ -285,6 +299,7 @@ export class WebsocketProvider extends ObservableV2 { this.serverUrl = serverUrl this.bcChannel = serverUrl + '/' + roomname this.maxBackoffTime = maxBackoffTime + this.maxBackoffIntervalOnSuccessfulConnects = maxBackoffIntervalOnSuccessfulConnects /** * The specified url parameters. This can be safely updated. The changed parameters will be used * when a new connection is established. @@ -301,6 +316,8 @@ export class WebsocketProvider extends ObservableV2 { this.bcconnected = false this.disableBc = disableBc this.wsUnsuccessfulReconnects = 0 + this.wsLastConnectAt = null + this.wsSubsequentConnects = 0 this.messageHandlers = messageHandlers.slice() /** * @type {boolean}