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": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
|
||||||
"@webcomponents/custom-elements": "^1.2.4",
|
"@webcomponents/custom-elements": "^1.2.4",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
|
@ -156,7 +155,8 @@
|
||||||
"ImageData",
|
"ImageData",
|
||||||
"OffscreenCanvas",
|
"OffscreenCanvas",
|
||||||
"postMessage",
|
"postMessage",
|
||||||
"getComputedStyle"
|
"getComputedStyle",
|
||||||
|
"WebSocket"
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"dist",
|
"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 lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
|
||||||
import { getStreamUrl } from './getStreamUrl'
|
import { getStreamUrl } from './getStreamUrl'
|
||||||
import { EventEmitter } from 'events-light'
|
import { EventEmitter } from 'events-light'
|
||||||
|
@ -11,7 +11,9 @@ export class TimelineStream extends EventEmitter {
|
||||||
this._accessToken = accessToken
|
this._accessToken = accessToken
|
||||||
this._timeline = timeline
|
this._timeline = timeline
|
||||||
this._onStateChange = this._onStateChange.bind(this)
|
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._setupWebSocket()
|
||||||
this._setupEvents()
|
this._setupEvents()
|
||||||
}
|
}
|
||||||
|
@ -40,7 +42,7 @@ export class TimelineStream extends EventEmitter {
|
||||||
|
|
||||||
_setupWebSocket () {
|
_setupWebSocket () {
|
||||||
const url = getStreamUrl(this._streamingApi, this._accessToken, this._timeline)
|
const url = getStreamUrl(this._streamingApi, this._accessToken, this._timeline)
|
||||||
const ws = new WebSocketClient(url, null, { backoff: 'fibonacci' })
|
const ws = new WebSocketClient(url)
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
if (!this._opened) {
|
if (!this._opened) {
|
||||||
|
@ -63,12 +65,16 @@ export class TimelineStream extends EventEmitter {
|
||||||
|
|
||||||
_setupEvents () {
|
_setupEvents () {
|
||||||
lifecycle.addEventListener('statechange', this._onStateChange)
|
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 () {
|
_teardownEvents () {
|
||||||
lifecycle.removeEventListener('statechange', this._onStateChange)
|
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 () {
|
_pause () {
|
||||||
|
@ -95,9 +101,24 @@ export class TimelineStream extends EventEmitter {
|
||||||
console.log('unfrozen')
|
console.log('unfrozen')
|
||||||
this._unpause()
|
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) {
|
if (online) {
|
||||||
console.log('online forced')
|
console.log('online forced')
|
||||||
this._unpause()
|
this._unpause()
|
||||||
|
@ -106,4 +127,14 @@ export class TimelineStream extends EventEmitter {
|
||||||
this._pause()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/routes/_thirdparty/websocket/LICENSE
vendored
Normal file
21
src/routes/_thirdparty/websocket/LICENSE
vendored
Normal file
|
@ -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.
|
36
src/routes/_thirdparty/websocket/backoff.js
vendored
Normal file
36
src/routes/_thirdparty/websocket/backoff.js
vendored
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
242
src/routes/_thirdparty/websocket/websocket.js
vendored
Normal file
242
src/routes/_thirdparty/websocket/websocket.js
vendored
Normal file
|
@ -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"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@mrmlnc/readdir-enhanced@^2.2.1":
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
|
||||||
|
|
Loading…
Reference in a new issue