fix: reset websocket on online/offline/active events (#1429)
* fix: reset websocket on online/offline/active events * minor fixup * add comments
This commit is contained in:
parent
88ab0b929c
commit
7b32c71c93
|
@ -44,7 +44,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@webcomponents/custom-elements": "^1.2.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
|
@ -156,7 +155,8 @@
|
|||
"ImageData",
|
||||
"OffscreenCanvas",
|
||||
"postMessage",
|
||||
"getComputedStyle"
|
||||
"getComputedStyle",
|
||||
"WebSocket"
|
||||
],
|
||||
"ignore": [
|
||||
"dist",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import WebSocketClient from '@gamestdio/websocket'
|
||||
import { WebSocketClient } from '../../_thirdparty/websocket/websocket'
|
||||
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
|
||||
import { getStreamUrl } from './getStreamUrl'
|
||||
import { EventEmitter } from 'events-light'
|
||||
|
@ -11,7 +11,9 @@ export class TimelineStream extends EventEmitter {
|
|||
this._accessToken = accessToken
|
||||
this._timeline = timeline
|
||||
this._onStateChange = this._onStateChange.bind(this)
|
||||
this._onOnlineForced = this._onOnlineForced.bind(this)
|
||||
this._onOnline = this._onOnline.bind(this)
|
||||
this._onOffline = this._onOffline.bind(this)
|
||||
this._onForcedOnlineStateChange = this._onForcedOnlineStateChange.bind(this)
|
||||
this._setupWebSocket()
|
||||
this._setupEvents()
|
||||
}
|
||||
|
@ -40,7 +42,7 @@ export class TimelineStream extends EventEmitter {
|
|||
|
||||
_setupWebSocket () {
|
||||
const url = getStreamUrl(this._streamingApi, this._accessToken, this._timeline)
|
||||
const ws = new WebSocketClient(url, null, { backoff: 'fibonacci' })
|
||||
const ws = new WebSocketClient(url)
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!this._opened) {
|
||||
|
@ -63,12 +65,16 @@ export class TimelineStream extends EventEmitter {
|
|||
|
||||
_setupEvents () {
|
||||
lifecycle.addEventListener('statechange', this._onStateChange)
|
||||
eventBus.on('forcedOnline', this._onOnlineForced)
|
||||
eventBus.on('forcedOnline', this._onForcedOnlineStateChange) // only happens in tests
|
||||
window.addEventListener('online', this._onOnline)
|
||||
window.addEventListener('offline', this._onOffline)
|
||||
}
|
||||
|
||||
_teardownEvents () {
|
||||
lifecycle.removeEventListener('statechange', this._onStateChange)
|
||||
eventBus.removeListener('forcedOnline', this._onOnlineForced)
|
||||
eventBus.removeListener('forcedOnline', this._onForcedOnlineStateChange) // only happens in tests
|
||||
window.removeEventListener('online', this._onOnline)
|
||||
window.removeEventListener('offline', this._onOffline)
|
||||
}
|
||||
|
||||
_pause () {
|
||||
|
@ -95,9 +101,24 @@ export class TimelineStream extends EventEmitter {
|
|||
console.log('unfrozen')
|
||||
this._unpause()
|
||||
}
|
||||
if (event.newState === 'active') { // page is reopened from a background tab
|
||||
console.log('active')
|
||||
this._tryToReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
_onOnlineForced (online) {
|
||||
_onOnline () {
|
||||
console.log('online')
|
||||
this._unpause() // if we're not paused, then this is a no-op
|
||||
this._tryToReconnect() // to be safe, try to reset and reconnect
|
||||
}
|
||||
|
||||
_onOffline () {
|
||||
console.log('offline')
|
||||
this._pause() // in testing, it seems to work better to stop polling when we get this event
|
||||
}
|
||||
|
||||
_onForcedOnlineStateChange (online) {
|
||||
if (online) {
|
||||
console.log('online forced')
|
||||
this._unpause()
|
||||
|
@ -106,4 +127,14 @@ export class TimelineStream extends EventEmitter {
|
|||
this._pause()
|
||||
}
|
||||
}
|
||||
|
||||
_tryToReconnect () {
|
||||
console.log('websocket readyState', this._ws && this._ws.readyState)
|
||||
if (this._ws && this._ws.readyState !== WebSocketClient.OPEN) {
|
||||
// if a websocket connection is not currently open, then reset the
|
||||
// backoff counter to ensure that fresh notifications come in faster
|
||||
this._ws.reset()
|
||||
this._ws.reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (c) 2015 Endel Dreyer
|
||||
|
||||
MIT License:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,36 @@
|
|||
const MAX_DELAY = 60000 // 60 seconds
|
||||
const INITIAL_DELAY = 100
|
||||
|
||||
export class Backoff {
|
||||
constructor (onReady) {
|
||||
this.attempts = 0
|
||||
this.onReady = onReady
|
||||
}
|
||||
|
||||
backoff () {
|
||||
const delay = this.fibonacci(++this.attempts)
|
||||
console.log('websocket delay', delay)
|
||||
setTimeout(this.onReady, delay)
|
||||
}
|
||||
|
||||
fibonacci (attempt) {
|
||||
let current = 1
|
||||
|
||||
if (attempt > current) {
|
||||
let prev = 1
|
||||
current = 2
|
||||
|
||||
for (let index = 2; index < attempt; index++) {
|
||||
const next = prev + current
|
||||
prev = current
|
||||
current = next
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(MAX_DELAY, Math.floor(Math.random() * current * INITIAL_DELAY))
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.attempts = 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
// forked from https://github.com/gamestdio/websocket/blob/4111bfa/src/index.js
|
||||
|
||||
import { Backoff } from './backoff'
|
||||
|
||||
export class WebSocketClient {
|
||||
/**
|
||||
* @param url DOMString The URL to which to connect; this should be the URL to which the WebSocket server will respond.
|
||||
* @param protocols DOMString|DOMString[] Either a single protocol string or an array of protocol strings. These strings are used to indicate sub-protocols, so that a single server can implement multiple WebSocket sub-protocols (for example, you might want one server to be able to handle different types of interactions depending on the specified protocol). If you don't specify a protocol string, an empty string is assumed.
|
||||
* @param options options
|
||||
*/
|
||||
constructor (url, protocols = null, options = {}) {
|
||||
this.url = url
|
||||
this.protocols = protocols
|
||||
|
||||
this.reconnectEnabled = true
|
||||
this.listeners = {}
|
||||
|
||||
this.backoff = new Backoff(this.onBackoffReady.bind(this))
|
||||
|
||||
if (typeof (options.connect) === 'undefined' || options.connect) {
|
||||
this.open()
|
||||
}
|
||||
}
|
||||
|
||||
open (reconnect = false) {
|
||||
this.isReconnect = reconnect
|
||||
|
||||
// keep binaryType used on previous WebSocket connection
|
||||
const binaryType = this.ws && this.ws.binaryType
|
||||
|
||||
this.ws = new WebSocket(this.url, this.protocols)
|
||||
this.ws.onclose = this.onCloseCallback.bind(this)
|
||||
this.ws.onerror = this.onErrorCallback.bind(this)
|
||||
this.ws.onmessage = this.onMessageCallback.bind(this)
|
||||
this.ws.onopen = this.onOpenCallback.bind(this)
|
||||
|
||||
if (binaryType) {
|
||||
this.ws.binaryType = binaryType
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
onBackoffReady () {
|
||||
this.open(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
onCloseCallback (e) {
|
||||
if (!this.isReconnect && this.listeners.onclose) {
|
||||
this.listeners.onclose.apply(null, arguments)
|
||||
}
|
||||
if (this.reconnectEnabled && e.code < 3000) {
|
||||
this.backoff.backoff()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
onErrorCallback () {
|
||||
if (this.listeners.onerror) {
|
||||
this.listeners.onerror.apply(null, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
onMessageCallback () {
|
||||
if (this.listeners.onmessage) {
|
||||
this.listeners.onmessage.apply(null, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
onOpenCallback () {
|
||||
if (this.listeners.onopen) {
|
||||
this.listeners.onopen.apply(null, arguments)
|
||||
}
|
||||
|
||||
if (this.isReconnect && this.listeners.onreconnect) {
|
||||
this.listeners.onreconnect.apply(null, arguments)
|
||||
}
|
||||
|
||||
this.isReconnect = false
|
||||
}
|
||||
|
||||
// Unused
|
||||
// /**
|
||||
// * The number of bytes of data that have been queued using calls to send()
|
||||
// * but not yet transmitted to the network. This value does not reset to zero
|
||||
// * when the connection is closed; if you keep calling send(), this will
|
||||
// * continue to climb.
|
||||
// *
|
||||
// * @type unsigned long
|
||||
// * @readonly
|
||||
// */
|
||||
// get bufferedAmount () { return this.ws.bufferedAmount }
|
||||
//
|
||||
/**
|
||||
* The current state of the connection; this is one of the Ready state constants.
|
||||
* @type unsigned short
|
||||
* @readonly
|
||||
*/
|
||||
get readyState () { return this.ws.readyState }
|
||||
|
||||
// Unused
|
||||
//
|
||||
// /**
|
||||
// * A string indicating the type of binary data being transmitted by the
|
||||
// * connection. This should be either "blob" if DOM Blob objects are being
|
||||
// * used or "arraybuffer" if ArrayBuffer objects are being used.
|
||||
// * @type DOMString
|
||||
// */
|
||||
// get binaryType () { return this.ws.binaryType }
|
||||
//
|
||||
// set binaryType (binaryType) { this.ws.binaryType = binaryType }
|
||||
//
|
||||
// /**
|
||||
// * The extensions selected by the server. This is currently only the empty
|
||||
// * string or a list of extensions as negotiated by the connection.
|
||||
// * @type DOMString
|
||||
// */
|
||||
// get extensions () { return this.ws.extensions }
|
||||
//
|
||||
// set extensions (extensions) { this.ws.extensions = extensions }
|
||||
//
|
||||
// /**
|
||||
// * A string indicating the name of the sub-protocol the server selected;
|
||||
// * this will be one of the strings specified in the protocols parameter when
|
||||
// * creating the WebSocket object.
|
||||
// * @type DOMString
|
||||
// */
|
||||
// get protocol () { return this.ws.protocol }
|
||||
//
|
||||
// set protocol (protocol) { this.ws.protocol = protocol }
|
||||
|
||||
/**
|
||||
* Closes the WebSocket connection or connection attempt, if any. If the
|
||||
* connection is already CLOSED, this method does nothing.
|
||||
*
|
||||
* @param code A numeric value indicating the status code explaining why the connection is being closed. If this parameter is not specified, a default value of 1000 (indicating a normal "transaction complete" closure) is assumed. See the list of status codes on the CloseEvent page for permitted values.
|
||||
* @param reason A human-readable string explaining why the connection is closing. This string must be no longer than 123 bytes of UTF-8 text (not characters).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
close (code, reason) {
|
||||
if (typeof code === 'undefined') { code = 1000 }
|
||||
|
||||
this.reconnectEnabled = false
|
||||
|
||||
this.ws.close(code, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits data to the server over the WebSocket connection.
|
||||
* @param data DOMString|ArrayBuffer|Blob
|
||||
* @return void
|
||||
*/
|
||||
send (data) { this.ws.send(data) }
|
||||
|
||||
/**
|
||||
* An event listener to be called when the WebSocket connection's readyState changes to CLOSED. The listener receives a CloseEvent named "close".
|
||||
* @param listener EventListener
|
||||
*/
|
||||
set onclose (listener) { this.listeners.onclose = listener }
|
||||
|
||||
get onclose () { return this.listeners.onclose }
|
||||
|
||||
/**
|
||||
* An event listener to be called when an error occurs. This is a simple event named "error".
|
||||
* @param listener EventListener
|
||||
*/
|
||||
set onerror (listener) { this.listeners.onerror = listener }
|
||||
|
||||
get onerror () { return this.listeners.onerror }
|
||||
|
||||
/**
|
||||
* An event listener to be called when a message is received from the server. The listener receives a MessageEvent named "message".
|
||||
* @param listener EventListener
|
||||
*/
|
||||
set onmessage (listener) { this.listeners.onmessage = listener }
|
||||
|
||||
get onmessage () { return this.listeners.onmessage }
|
||||
|
||||
/**
|
||||
* An event listener to be called when the WebSocket connection's readyState changes to OPEN; this indicates that the connection is ready to send and receive data. The event is a simple one with the name "open".
|
||||
* @param listener EventListener
|
||||
*/
|
||||
set onopen (listener) { this.listeners.onopen = listener }
|
||||
|
||||
get onopen () { return this.listeners.onopen }
|
||||
|
||||
/**
|
||||
* @param listener EventListener
|
||||
*/
|
||||
set onreconnect (listener) { this.listeners.onreconnect = listener }
|
||||
|
||||
get onreconnect () { return this.listeners.onreconnect }
|
||||
|
||||
/**
|
||||
* Reset the backoff function back to initial state
|
||||
*/
|
||||
reset () {
|
||||
console.log('websocket reset')
|
||||
this.backoff.reset()
|
||||
}
|
||||
|
||||
/** Reconnect the websocket
|
||||
*
|
||||
*/
|
||||
reconnect () {
|
||||
console.log('websocket reconnect')
|
||||
this.onBackoffReady()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The connection is not yet open.
|
||||
*/
|
||||
WebSocketClient.CONNECTING = WebSocket.CONNECTING
|
||||
|
||||
/**
|
||||
* The connection is open and ready to communicate.
|
||||
*/
|
||||
WebSocketClient.OPEN = WebSocket.OPEN
|
||||
|
||||
/**
|
||||
* The connection is in the process of closing.
|
||||
*/
|
||||
WebSocketClient.CLOSING = WebSocket.CLOSING
|
||||
|
||||
/**
|
||||
* The connection is closed or couldn't be opened.
|
||||
*/
|
||||
WebSocketClient.CLOSED = WebSocket.CLOSED
|
|
@ -231,11 +231,6 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@gamestdio/websocket@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@gamestdio/websocket/-/websocket-0.3.2.tgz#321ba0976ee30fd14e51dbf8faa85ce7b325f76a"
|
||||
integrity sha512-J3n5SKim+ZoLbe44hRGI/VYAwSMCeIJuBy+FfP6EZaujEpNchPRFcIsVQLWAwpU1bP2Ji63rC+rEUOd1vjUB6Q==
|
||||
|
||||
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
||||
|
|
Loading…
Reference in New Issue