Skip to content

Commit

Permalink
feat(websockets): remove polling and add middleware for websockets
Browse files Browse the repository at this point in the history
Merge pull request #452 from qri-io/websockets
  • Loading branch information
ramfox authored Feb 21, 2020
2 parents 43332e6 + b454ff4 commit a5ec274
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 155 deletions.
11 changes: 9 additions & 2 deletions app/actions/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,16 @@ export function fetchWorkingStatus (): ApiActionThunk {
// to invalidate pagination, set page to -1
export function fetchBody (page: number = 1, pageSize: number = bodyPageSizeDefault): ApiActionThunk {
return async (dispatch, getState) => {
const { workingDataset, selections } = getState()
const { workingDataset, selections, myDatasets } = getState()
const { peername, name } = selections
const { path, fsiPath } = workingDataset
const { path } = workingDataset

// look up the peername + name in myDatasets to determine whether it is FSI linked
const dataset = myDatasets.value.find((d) => (d.username === peername) && (d.name === name))
if (!dataset) {
return Promise.reject(new Error('could not find dataset in list'))
}
const { fsiPath } = dataset

const { page: confirmedPage, doNotFetch } = actionWithPagination(page, workingDataset.components.body.pageInfo)

Expand Down
11 changes: 6 additions & 5 deletions app/actions/session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CALL_API, ApiActionThunk, chainSuccess } from '../store/api'
import { CALL_API, ApiActionThunk } from '../store/api'
import { Session } from '../models/session'
import { Action } from 'redux'
import { AnyAction } from 'redux'
import { fetchMyDatasets } from './api'
import { wsConnect } from '../store/wsMiddleware'

export function fetchSession (): ApiActionThunk {
return async (dispatch) => {
Expand All @@ -21,11 +22,11 @@ export function fetchSession (): ApiActionThunk {

export function bootstrap (): ApiActionThunk {
return async (dispatch, getState) => {
const whenOk = chainSuccess(dispatch, getState)
let response: Action
let response: AnyAction

response = await fetchSession()(dispatch, getState)
response = await whenOk(fetchMyDatasets(-1))(response)
.then(() => dispatch(wsConnect()))
.then(async () => fetchMyDatasets(-1)(dispatch, getState))

return response
}
Expand Down
26 changes: 2 additions & 24 deletions app/components/workbench/Workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CSSTransition } from 'react-transition-group'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFolderOpen, faFile, faLink, faCloud, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'

import { QRI_CLOUD_URL, DEFAULT_POLL_INTERVAL } from '../../constants'
import { QRI_CLOUD_URL } from '../../constants'
import { Details } from '../../models/details'
import { Session } from '../../models/session'
import {
Expand Down Expand Up @@ -81,8 +81,6 @@ export interface WorkbenchProps {
}

class Workbench extends React.Component<WorkbenchProps> {
poll: NodeJS.Timeout

constructor (props: WorkbenchProps) {
super(props);

Expand All @@ -91,9 +89,7 @@ class Workbench extends React.Component<WorkbenchProps> {
'publishUnpublishDataset',
'handleShowStatus',
'handleShowHistory',
'handleCopyLink',
'startPolling',
'stopPolling'
'handleCopyLink'
].forEach((m) => { this[m] = this[m].bind(this) })
}

Expand All @@ -105,33 +101,15 @@ class Workbench extends React.Component<WorkbenchProps> {
ipcRenderer.on('publish-unpublish-dataset', this.publishUnpublishDataset)

this.props.fetchWorkbench()
this.startPolling()
}

componentWillUnmount () {
this.stopPolling()
ipcRenderer.removeListener('show-status', this.handleShowStatus)
ipcRenderer.removeListener('show-history', this.handleShowHistory)
ipcRenderer.removeListener('open-working-directory', this.openWorkingDirectory)
ipcRenderer.removeListener('publish-unpublish-dataset', this.publishUnpublishDataset)
}

private startPolling () {
if (this.poll) {
clearInterval(this.poll)
}

this.poll = setInterval(() => {
if (this.props.data.workingDataset.peername !== '' || this.props.data.workingDataset.name !== '') {
this.props.fetchWorkingStatus()
}
}, DEFAULT_POLL_INTERVAL)
}

private stopPolling () {
clearInterval(this.poll)
}

private handleShowStatus () {
this.props.setActiveTab('status')
}
Expand Down
6 changes: 5 additions & 1 deletion app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const DISCORD_URL = 'https://discordapp.com/invite/thkJHKj'
const QRI_CLOUD_URL = 'https://qri.cloud'
const BACKEND_URL = 'http://localhost:2503'
const WEBSOCKETS_URL = 'ws://localhost:2506'
const WEBSOCKETS_PROTOCOL = 'qri-websocket'
// 3000ms is quick enough for the app to feel responsive
// but is slow enough to not trip up the backend
const DEFAULT_POLL_INTERVAL = 3000
Expand All @@ -11,5 +13,7 @@ module.exports = {
BACKEND_URL,
DEFAULT_POLL_INTERVAL,
DISCORD_URL,
QRI_CLOUD_URL
QRI_CLOUD_URL,
WEBSOCKETS_URL,
WEBSOCKETS_PROTOCOL
}
3 changes: 2 additions & 1 deletion app/store/configureStore.development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { routerMiddleware, push } from 'connected-react-router'
// import { createLogger } from 'redux-logger'
import createRootReducer from '../reducers'
import { apiMiddleware } from './api'
import wsMiddleware from './wsMiddleware'

declare const window: Window & {
__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?(a: any): void
Expand Down Expand Up @@ -39,7 +40,7 @@ const composeEnhancers: typeof compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPO
: compose
/* eslint-enable no-underscore-dangle */
const enhancer = composeEnhancers(
applyMiddleware(thunk, router, apiMiddleware)
applyMiddleware(thunk, router, apiMiddleware, wsMiddleware)
)

const configureStore = (initialState: Object | void) => {
Expand Down
3 changes: 2 additions & 1 deletion app/store/configureStore.production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { createHashHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import createRootReducer from '../reducers'
import { apiMiddleware } from './api'
import wsMiddleware from './wsMiddleware'

const history = createHashHistory()
const router = routerMiddleware(history)
const enhancer = applyMiddleware(thunk, router, apiMiddleware)
const enhancer = applyMiddleware(thunk, router, apiMiddleware, wsMiddleware)

const configureStore = (initialState: Object | void) => {
return createStore(createRootReducer(history), initialState as any, enhancer)
Expand Down
82 changes: 82 additions & 0 deletions app/store/wsMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Dispatch, AnyAction, Store } from 'redux'

import { WEBSOCKETS_URL, WEBSOCKETS_PROTOCOL } from '../constants'

import { Store as IStore } from '../models/store'
import { fetchWorkingStatus } from '../actions/api'

// wsMiddleware manages requests to connect to the qri backend via websockets
// as well as managing messages that get passed through
export const wsConnect = () => ({ type: 'WS_CONNECT' })
export const wsConnecting = () => ({ type: 'WS_CONNECTING' })
export const wsConnected = () => ({ type: 'WS_CONNECTED' })
export const wsDisconnect = () => ({ type: 'WS_DISCONNECT' })
export const wsDisconnected = () => ({ type: 'WS_DISCONNECTED' })

const socketMiddleware = () => {
let socket: WebSocket = null

const onOpen = (store: Store<IStore>) => (event: Event) => {
if (socket !== null) return
store.dispatch(wsConnect())
}

const onClose = (store: Store<IStore>) => () => {
store.dispatch(wsDisconnected())
}

const onMessage = (store: Store<IStore>) => (event: MessageEvent) => {
const payload = JSON.parse(event.data)
switch (payload.Type) {
case 'modify':
case 'create':
case 'remove':
const { workingDataset } = store.getState()
const { peername, name } = workingDataset
// if the websocket message Username and Dsname match the peername and
// dataset name of the dataset that is currently being viewed, fetch
// status
if (peername && name && peername === payload.Username && name === payload.Dsname) {
fetchWorkingStatus()(store.dispatch, store.getState)
}
break
default:
console.log('default')
}
}

// middleware
return (store: Store<IStore>) => (next: Dispatch<AnyAction>) => (action: AnyAction) => {
switch (action.type) {
case 'WS_CONNECT':
if (socket !== null) {
socket.close()
}

// connect to the remote host
socket = new WebSocket(WEBSOCKETS_URL, WEBSOCKETS_PROTOCOL)

// websocket handlers
socket.onmessage = onMessage(store)
socket.onclose = onClose(store)
socket.onopen = onOpen(store)

break
case 'WS_DISCONNECT':
if (socket !== null) {
socket.close()
}
socket = null
console.log('websocket closed')
break
// case 'NEW_MESSAGE':
// console.log('sending a message', action.msg)
// socket.send(JSON.stringify({ command: 'NEW_MESSAGE', message: action.msg }))
// break
default:
return next(action)
}
}
}

export default socketMiddleware()
Loading

0 comments on commit a5ec274

Please sign in to comment.