diff --git a/bin/browser-shim.js b/bin/browser-shim.js new file mode 100644 index 0000000..15f5310 --- /dev/null +++ b/bin/browser-shim.js @@ -0,0 +1,9 @@ +// browser shims that run in node, so that page-lifecycle won't error +global.self = global +global.document = { + visibilityState: 'visible', + hasFocus: () => true, + wasDiscarded: false +} +global.addEventListener = () => {} +global.removeEventListener = () => {} diff --git a/package.json b/package.json index 579e598..4089eae 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "testcafe": "run-s testcafe-suite0 testcafe-suite1", "testcafe-suite0": "cross-env-shell testcafe --hostname localhost --skip-js-errors -c 4 $BROWSER tests/spec/0*", "testcafe-suite1": "cross-env-shell testcafe --hostname localhost --skip-js-errors $BROWSER tests/spec/1*", - "test-unit": "mocha -r esm tests/unit/", + "test-unit": "mocha -r esm -r bin/browser-shim.js tests/unit/", "wait-for-mastodon-to-start": "node -r esm bin/wait-for-mastodon-to-start.js", "wait-for-mastodon-data": "node -r esm bin/wait-for-mastodon-data.js", "deploy-prod": "DEPLOY_TYPE=prod ./bin/deploy.sh", diff --git a/src/routes/_api/TimelineStream.js b/src/routes/_api/TimelineStream.js index 31501d7..69be738 100644 --- a/src/routes/_api/TimelineStream.js +++ b/src/routes/_api/TimelineStream.js @@ -1,6 +1,7 @@ import { paramsString } from '../_utils/ajax' import noop from 'lodash-es/noop' -import { importWebSocketClient } from '../_utils/asyncModules' +import WebSocketClient from '@gamestdio/websocket' +import lifecycle from 'page-lifecycle/dist/lifecycle.mjs' function getStreamName (timeline) { switch (timeline) { @@ -46,27 +47,60 @@ function getUrl (streamingApi, accessToken, timeline) { export class TimelineStream { constructor (streamingApi, accessToken, timeline, opts) { - let url = getUrl(streamingApi, accessToken, timeline) - importWebSocketClient().then(WebSocketClient => { - if (this.__closed) { - return - } - const ws = new WebSocketClient(url, null, { backoff: 'exponential' }) - const onMessage = opts.onMessage || noop - - ws.onopen = opts.onOpen || noop - ws.onmessage = e => onMessage(JSON.parse(e.data)) - ws.onclose = opts.onClose || noop - ws.onreconnect = opts.onReconnect || noop - - this._ws = ws - }) + this._streamingApi = streamingApi + this._accessToken = accessToken + this._timeline = timeline + this._opts = opts + this._onStateChange = this._onStateChange.bind(this) + this._setupWebSocket() + this._setupLifecycle() } close () { - this.__closed = true + this._closed = true + this._closeWebSocket() + this._teardownLifecycle() + } + + _closeWebSocket () { if (this._ws) { this._ws.close() + this._ws = null + } + } + + _setupWebSocket () { + const url = getUrl(this._streamingApi, this._accessToken, this._timeline) + const ws = new WebSocketClient(url, null, { backoff: 'exponential' }) + + ws.onopen = this._opts.onOpen || noop + ws.onmessage = this._opts.onMessage ? e => this._opts.onMessage(JSON.parse(e.data)) : noop + ws.onclose = this._opts.onClose || noop + ws.onreconnect = this._opts.onReconnect || noop + + this._ws = ws + } + + _setupLifecycle () { + lifecycle.addEventListener('statechange', this._onStateChange) + } + + _teardownLifecycle () { + lifecycle.removeEventListener('statechange', this._onStateChange) + } + + _onStateChange (event) { + if (this._closed) { + return + } + // when the page enters or exits a frozen state, pause or resume websocket polling + if (event.newState === 'frozen') { // page is frozen + console.log('frozen') + this._closeWebSocket() + } else if (event.oldState === 'frozen') { // page is unfrozen + console.log('unfrozen') + this._closeWebSocket() + this._setupWebSocket() } } } diff --git a/src/routes/_store/LocalStorageStore.js b/src/routes/_store/LocalStorageStore.js index a66773d..4c1bb8d 100644 --- a/src/routes/_store/LocalStorageStore.js +++ b/src/routes/_store/LocalStorageStore.js @@ -1,6 +1,6 @@ import { Store } from 'svelte/store' import { safeLocalStorage as LS } from '../_utils/safeLocalStorage' -import { importPageLifecycle } from '../_utils/asyncModules' +import lifecycle from 'page-lifecycle/dist/lifecycle.mjs' function safeParse (str) { return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str)) @@ -31,13 +31,11 @@ export class LocalStorageStore extends Store { }) }) if (process.browser) { - importPageLifecycle().then(lifecycle => { - lifecycle.addEventListener('statechange', e => { - if (e.newState === 'passive') { - console.log('saving LocalStorageStore...') - this.save() - } - }) + lifecycle.addEventListener('statechange', e => { + if (e.newState === 'passive') { + console.log('saving LocalStorageStore...') + this.save() + } }) } } diff --git a/src/routes/_utils/asyncModules.js b/src/routes/_utils/asyncModules.js index 16d27d2..9950e1a 100644 --- a/src/routes/_utils/asyncModules.js +++ b/src/routes/_utils/asyncModules.js @@ -4,14 +4,6 @@ export const importTimeline = () => import( /* webpackChunkName: 'Timeline' */ '../_components/timeline/Timeline.html' ).then(getDefault) -export const importPageLifecycle = () => import( - /* webpackChunkName: 'page-lifecycle' */ 'page-lifecycle/dist/lifecycle.mjs' - ).then(getDefault) - -export const importWebSocketClient = () => import( - /* webpackChunkName: '@gamestdio/websocket' */ '@gamestdio/websocket' - ).then(getDefault) - export const importVirtualList = () => import( /* webpackChunkName: 'VirtualList.html' */ '../_components/virtualList/VirtualList.html' ).then(getDefault) diff --git a/webpack/server.config.js b/webpack/server.config.js index 09958ef..7a529d7 100644 --- a/webpack/server.config.js +++ b/webpack/server.config.js @@ -3,11 +3,14 @@ const config = require('sapper/config/webpack.js') const pkg = require('../package.json') const { mode, dev, resolve, inlineSvgs } = require('./shared.config') +const serverResolve = JSON.parse(JSON.stringify(resolve)) +serverResolve.alias['page-lifecycle/dist/lifecycle.mjs'] = 'lodash-es/noop' // page lifecycle fails in Node + module.exports = { entry: config.server.entry(), output: config.server.output(), target: 'node', - resolve, + resolve: serverResolve, externals: Object.keys(pkg.dependencies), module: { rules: [