Skip to content

Commit

Permalink
Merge pull request #167 from Coding/zhengxinqi/offline
Browse files Browse the repository at this point in the history
add offline reconnect logic
  • Loading branch information
hackape committed Aug 17, 2017
2 parents 951f612 + 7472d39 commit 2799609
Show file tree
Hide file tree
Showing 11 changed files with 186 additions and 28 deletions.
42 changes: 25 additions & 17 deletions app/backendAPI/websocketClients.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Stomp } from 'stompjs/lib/stomp'
import SockJS from 'sockjs-client'
import getBackoff from 'utils/getBackoff'
import emitter, * as E from 'utils/emitter'
import config from 'config'
import { autorun, runInAction } from 'mobx'
import { notify, NOTIFY_TYPE } from '../components/Notification/actions'

const log = console.log || (x => x)
const warn = console.warn || (x => x)
Expand All @@ -23,40 +25,40 @@ class FsSocketClient {
})
this.maxAttempts = 7
FsSocketClient.$$singleton = this
emitter.on(E.SOCKET_RETRY, this.reconnect.bind(this))
}

connect (connectCallback, errorCallback) {
const self = this
connect () {
if (!this.socket || !this.stompClient) {
this.socket = new SockJS(...this.sockJSConfigs)
this.stompClient = Stomp.over(this.socket)
this.stompClient.debug = false // stop logging PING/PONG
}
self.stompClient.connect({}, function success () {
const success = () => {
runInAction(() => config.fsSocketConnected = true)
self.backoff.reset()
connectCallback.call(this)
}, function error (e) {
log('fsSocket error', self.socket)
switch (self.socket.readyState) {
this.backoff.reset()
this.successCallback(this.stompClient)
}
const error = (frame) => {
log('fsSocket error', this.socket)
switch (this.socket.readyState) {
case SockJS.CLOSING:
case SockJS.CLOSED:
runInAction(() => config.fsSocketConnected = false)
self.reconnect(connectCallback, errorCallback)
this.reconnect()
break
case SockJS.OPEN:
log('FRAME ERROR', arguments[0])
log('FRAME ERROR', frame)
break
default:
}
errorCallback(arguments)
})
self.socket.onclose = () => {
log('socket is closing')
this.errorCallback(frame)
}

this.stompClient.connect({}, success, error)
}

reconnect (connectCallback, errorCallback) {
reconnect () {
if (config.fsSocketConnected) return
log(`try reconnect fsSocket ${this.backoff.attempts}`)
// unset this.socket
Expand All @@ -65,14 +67,17 @@ class FsSocketClient {
const retryDelay = this.backoff.duration()
log(`Retry after ${retryDelay}ms`)
const timer = setTimeout(
this.connect.bind(this, connectCallback, errorCallback)
this.connect.bind(this)
, retryDelay)
} else {
emitter.emit(E.SOCKET_TRIED_FAILED)
notify({ message: i18n`global.onSocketError`, notifyType: NOTIFY_TYPE.ERROR })
this.backoff.reset()
warn('Sock connected failed, something may be broken, reload page and try again')
}
}
close (connectCallback) {

close () {
const self = this
if (config.fsSocketConnected) {
self.socket.close(1000, 123)
Expand Down Expand Up @@ -110,6 +115,9 @@ class TtySocketClient {
this.maxAttempts = 5

TtySocketClient.$$singleton = this
emitter.on(E.SOCKET_RETRY, () => {
this.reconnect()
})
return this
}

Expand Down
11 changes: 6 additions & 5 deletions app/backendAPI/workspaceAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ export function createWorkspace (options) {
}

export function connectWebsocketClient () {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const fsSocketClient = new FsSocketClient()
fsSocketClient.connect(function () {
connectedResolve(this)
fsSocketClient.successCallback = function (stompClient) {
connectedResolve(stompClient)
resolve(true)
}, (err) => {
})
}
fsSocketClient.errorCallback = function (err) {}
fsSocketClient.connect()
})
}

Expand Down
8 changes: 8 additions & 0 deletions app/components/MenuBar/MenuBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { isFunction } from 'utils/is'
import Menu from '../Menu'
import PluginArea from '../../components/Plugins/component'
import { MENUBAR } from '../../components/Plugins/constants'
import { injectComponent } from '../../components/Plugins/actions'
import Offline from '../../components/Offline/Offline'

class MenuBar extends Component {
static propTypes = {
Expand All @@ -16,6 +18,12 @@ class MenuBar extends Component {
super(props)
this.state = { activeItemIndex: -1 }
}
componentDidMount () {
injectComponent(MENUBAR.WIDGET, {
key: 'offlineController',
weight: 3,
}, () => Offline)
}
activateItemAtIndex = (index, isTogglingEnabled) => {
if (isTogglingEnabled && this.state.activeItemIndex == index) {
this.setState({ activeItemIndex: -1 })
Expand Down
72 changes: 72 additions & 0 deletions app/components/Offline/Offline.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { Component, PropTypes } from 'react'
import { inject } from 'mobx-react'
import { emitter, E } from 'utils'
import cx from 'classnames'
import i18n from 'utils/createI18n'
import config from '../../config'


// 三个状态 1. connected(两个socket全链接) 2. disconnected可点击 3. connecting
@inject(() => ({
fsSocketConnected: config.fsSocketConnected,
}))
class Offline extends Component {
state = {
showButton: false,
isConnecting: false,
}

componentDidMount () {
emitter.on(E.SOCKET_TRIED_FAILED, () => {
this.setState({ showButton: true, isConnecting: false })
})
}
componentWillReceiveProps (np) {
if ((np.fsSocketConnected !== this.props.fsSocketConnected) && (np.fsSocketConnected === true)) {
this.setState({ isConnecting: false }, () => {
setTimeout(() => {
this.setState({ showButton: false })
}, 10000)
})
}
}
render () {
const isConnecting = this.state.isConnecting
const isConnected = this.props.fsSocketConnected
if (!this.state.showButton) {
return null
}

let buttonStatusClass
if (isConnecting) {
buttonStatusClass = 'btn-primary'
} else if (isConnected) {
buttonStatusClass = 'btn-success'
} else {
buttonStatusClass = 'btn-danger'
}

return (<div
className={cx('offline-container btn toggle btn-xs', buttonStatusClass, {
on: isConnecting || isConnected,
off: !isConnecting && !isConnected,
blink: isConnecting,
})}
onClick={() => {
if (isConnected) return
this.setState({ isConnecting: true })
emitter.emit(E.SOCKET_RETRY)
}}
>
<div className='toggle-group'>
{i18n`global.online`}<div className='toggle-handle' />{i18n`global.offline`}
</div>
</div>)
}
}

Offline.propTypes = {
fsSocketConnected: PropTypes.bool
}

export default Offline
4 changes: 4 additions & 0 deletions app/i18n/en_US/global.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"loadingWorkspaceCloning": "Now cloning code...",
"loadingWorkspaceCloneFailed": "Clone failed",
"offline": "Offline",
"recloneWorkspace": "Try clone again",
"requestingCollaboration": "You have requested collaboration",
"online": "Online",
"loadingWorkspaceDenied": "Loading workspace denied",
"requestCollaborationReject": "Your request has been rejected.",
"loadingWorkspaceFailed": "Loading workspace is failed",
"onSocketError": "file socket is error",
"requestCollaboration": "Request collaboration",
"connecting": "Connecting",
"loadingWorkspace": "Loading workspace...",
"reloadWorkspace": "Retry",
"requestCollaborationExpires": "The demo has already expired."
Expand Down
4 changes: 4 additions & 0 deletions app/i18n/zh_CN/global.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
{
"loadingWorkspaceCloning": "正在 Clone 项目代码...",
"loadingWorkspaceCloneFailed": "Clone 代码失败",
"offline": "离线",
"recloneWorkspace": "重新 Clone 代码",
"requestingCollaboration": "协作申请已经提交",
"online": "在线",
"loadingWorkspaceDenied": "访问工作区未授权",
"requestCollaborationReject": "您的申请被拒绝了",
"loadingWorkspaceFailed": "加载工作区失败",
"onSocketError": "已断线",
"requestCollaboration": "申请协作",
"connecting": "连接",
"loadingWorkspace": "正在加载工作区",
"reloadWorkspace": "重新加载",
"requestCollaborationExpires": "试用已经结束"
Expand Down
6 changes: 1 addition & 5 deletions app/styles/core-ui/MenuBar.styl
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@
.btn-info {
margin-left: 10px;
line-height: 12px;
}
.btn-danger{
margin-left: 10px;
line-height: 12px;
height: 16px;
margin-right: 10px;
}
.share-btn {
margin-right: 10px;
Expand Down
61 changes: 61 additions & 0 deletions app/styles/core-ui/Offline.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.offline-container {
$container-width = 60px;
$toggle-handle-width = 10px;
$button-text-width = $container-width - $toggle-handle-width - 2px;

padding: 0;
width: $container-width;
height: 16px;
line-height: 1.2;
position: relative;
overflow: hidden;
noSelect()

.toggle-group {
position: absolute;
display: flex;
align-content: center;
transition: all 0.35s ease;
top: 0;
bottom: 0;

& > span {
width: $button-text-width;
display: inline-block;
text-align: center;
height: 100%
}
}

&.on .toggle-group {
left: 0px;
}

&.off .toggle-group {
left: -1 * $button-text-width;
}

.toggle-handle {
display: inline-block;
width: $toggle-handle-width;
background-color: #ffffff;
height: 100%;
border-radius: 2px;
}
}

.blink {
animation: blink 800ms infinite;
}

@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0.2;
}
100% {
opacity: 1;
}
}
1 change: 1 addition & 0 deletions app/styles/core-ui/index.styl
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
@import "./Accordion";
@import "./Initialize";
@import "./Editor";
@import "./Offline";
3 changes: 3 additions & 0 deletions app/utils/emitter/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import EventEmitter from 'eventemitter3'

export default new EventEmitter()

export const PANEL_RESIZED = 'PANEL_RESIZED'
export const THEME_CHANGED = 'THEME_CHANGED'
export const SOCKET_TRIED_FAILED = 'SOCKET_TRIED_FAILED'
export const SOCKET_RETRY = 'SOCKET_RETRY'
2 changes: 1 addition & 1 deletion app/utils/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const responseInterceptor = request.interceptors.response.use((response) => {
return response.data
}, (error) => {
responseRedirect(error.response)
if (error.response.data) Object.assign(error, error.response.data)
if (error.response && error.response.data) Object.assign(error, error.response.data)
return Promise.reject(error)
})

Expand Down

0 comments on commit 2799609

Please sign in to comment.