Skip to content

Commit 64f285f

Browse files
committed
implement real time client
1 parent c0baf13 commit 64f285f

27 files changed

+4251
-3758
lines changed

front-end/package-lock.json

Lines changed: 3729 additions & 3742 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

front-end/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"noty": "^3.2.0-beta",
2424
"npm": "^6.4.0",
2525
"popper.js": "^1.14.4",
26+
"sockjs-client": "^1.1.5",
2627
"vue": "^2.5.17",
2728
"vue-i18n": "^8.0.0",
2829
"vue-router": "^3.0.1",

front-end/src/App.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
import 'bootstrap/dist/js/bootstrap.min'
99
1010
export default {
11-
name: 'App'
11+
name: 'App',
12+
created () {
13+
this.$bus.$on('myDataFetched', myData => {
14+
// Initializing the real time connection
15+
this.$rt.init(myData.settings.realTimeServerUrl, myData.user.token)
16+
})
17+
}
1218
}
1319
</script>
1420

front-end/src/components/PageHeader.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ export default {
6060
'teamBoards'
6161
])
6262
},
63-
created () {
64-
this.$store.dispatch('getMyData')
63+
mounted () {
64+
if (!this.user.authenticated) {
65+
this.$store.dispatch('getMyData')
66+
}
6567
},
6668
methods: {
6769
goHome () {
@@ -71,7 +73,10 @@ export default {
7173
this.$router.push({name: 'board', params: { boardId: board.id }})
7274
},
7375
signOut () {
76+
this.$rt.logout()
77+
7478
meService.signOut().then(() => {
79+
this.$store.dispatch('logout')
7580
this.$router.push({name: 'login'})
7681
}).catch(error => {
7782
notify.error(error.message)

front-end/src/event-bus.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Vue from 'vue'
2+
3+
const bus = new Vue({})
4+
5+
export default bus

front-end/src/main.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { library as faLibrary } from '@fortawesome/fontawesome-svg-core'
88
import { faHome, faSearch, faPlus, faEllipsisH, faUserPlus, faListUl } from '@fortawesome/free-solid-svg-icons'
99
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
1010
import { i18n } from './i18n'
11+
import eventBus from './event-bus'
12+
import realTimeClient from '@/real-time-client'
1113

1214
// Bootstrap axios
1315
axios.defaults.baseURL = '/api'
@@ -28,6 +30,9 @@ Vue.component('font-awesome-icon', FontAwesomeIcon)
2830

2931
Vue.config.productionTip = false
3032

33+
Vue.prototype.$bus = eventBus
34+
Vue.prototype.$rt = realTimeClient
35+
3136
new Vue({
3237
router,
3338
store,

front-end/src/real-time-client.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import Vue from 'vue'
2+
import SockJS from 'sockjs-client'
3+
import globalBus from '@/event-bus'
4+
5+
class RealTimeClient {
6+
constructor () {
7+
this.serverUrl = null
8+
this.token = null
9+
this.socket = null
10+
// If the client is authenticated through real time connection or not
11+
this.authenticated = false
12+
this.loggedOut = false
13+
this.$bus = new Vue()
14+
this.subscribeQueue = {
15+
/* channel: [handler1, handler2] */
16+
}
17+
this.unsubscribeQueue = {
18+
/* channel: [handler1, handler2] */
19+
}
20+
}
21+
init (serverUrl, token) {
22+
if (this.authenticated) {
23+
console.warn('[RealTimeClient] WS connection already authenticated.')
24+
return
25+
}
26+
console.log('[RealTimeClient] Initializing')
27+
this.serverUrl = serverUrl
28+
this.token = token
29+
this.connect()
30+
}
31+
logout () {
32+
console.log('[RealTimeClient] Logging out')
33+
this.subscribeQueue = {}
34+
this.unsubscribeQueue = {}
35+
this.authenticated = false
36+
this.loggedOut = true
37+
this.socket && this.socket.close()
38+
}
39+
connect () {
40+
console.log('[RealTimeClient] Connecting to ' + this.serverUrl)
41+
this.socket = new SockJS(this.serverUrl + '?token=' + this.token)
42+
this.socket.onopen = () => {
43+
// Once the connection established, always set the client as authenticated
44+
this.authenticated = true
45+
this._onConnected()
46+
}
47+
this.socket.onmessage = (event) => {
48+
this._onMessageReceived(event)
49+
}
50+
this.socket.onerror = (error) => {
51+
this._onSocketError(error)
52+
}
53+
this.socket.onclose = (event) => {
54+
this._onClosed(event)
55+
}
56+
}
57+
subscribe (channel, handler) {
58+
if (!this._isConnected()) {
59+
this._addToSubscribeQueue(channel, handler)
60+
return
61+
}
62+
const message = {
63+
action: 'subscribe',
64+
channel
65+
}
66+
this._send(message)
67+
this.$bus.$on(this._channelEvent(channel), handler)
68+
console.log('[RealTimeClient] Subscribed to channel ' + channel)
69+
}
70+
unsubscribe (channel, handler) {
71+
// Already logged out, no need to unsubscribe
72+
if (this.loggedOut) {
73+
return
74+
}
75+
76+
if (!this._isConnected()) {
77+
this._addToUnsubscribeQueue(channel, handler)
78+
return
79+
}
80+
const message = {
81+
action: 'unsubscribe',
82+
channel
83+
}
84+
this._send(message)
85+
this.$bus.$off(this._channelEvent(channel), handler)
86+
console.log('[RealTimeClient] Unsubscribed from channel ' + channel)
87+
}
88+
_isConnected () {
89+
return this.socket && this.socket.readyState === SockJS.OPEN
90+
}
91+
_onConnected () {
92+
globalBus.$emit('RealTimeClient.connected')
93+
console.log('[RealTimeClient] Connected')
94+
95+
// Handle subscribe and unsubscribe queue
96+
this._processQueues()
97+
}
98+
_onMessageReceived (event) {
99+
const message = JSON.parse(event.data)
100+
console.log('[RealTimeClient] Received message', message)
101+
102+
if (message.channel) {
103+
this.$bus.$emit(this._channelEvent(message.channel), message.payload)
104+
}
105+
}
106+
_send (message) {
107+
this.socket.send(JSON.stringify(message))
108+
}
109+
_onSocketError (error) {
110+
console.error('[RealTimeClient] Socket error', error)
111+
}
112+
_onClosed (event) {
113+
console.log('[RealTimeClient] Received close event', event)
114+
if (this.loggedOut) {
115+
// Manually logged out, no need to reconnect
116+
console.log('[RealTimeClient] Logged out')
117+
globalBus.$emit('RealTimeClient.loggedOut')
118+
} else {
119+
// Temporarily disconnected, attempt reconnect
120+
console.log('[RealTimeClient] Disconnected')
121+
globalBus.$emit('RealTimeClient.disconnected')
122+
123+
setTimeout(() => {
124+
console.log('[RealTimeClient] Reconnecting')
125+
globalBus.$emit('RealTimeClient.reconnecting')
126+
this.connect()
127+
}, 1000)
128+
}
129+
}
130+
_channelEvent (channel) {
131+
return 'channel:' + channel
132+
}
133+
_processQueues () {
134+
console.log('[RealTimeClient] Processing subscribe/unsubscribe queues')
135+
136+
// Process subscribe queue
137+
const subscribeChannels = Object.keys(this.subscribeQueue)
138+
subscribeChannels.forEach(channel => {
139+
const handlers = this.subscribeQueue[channel]
140+
handlers.forEach(handler => {
141+
this.subscribe(channel, handler)
142+
this._removeFromQueue(this.subscribeQueue, channel, handler)
143+
})
144+
})
145+
146+
// Process unsubscribe queue
147+
const unsubscribeChannels = Object.keys(this.unsubscribeQueue)
148+
unsubscribeChannels.forEach(channel => {
149+
const handlers = this.unsubscribeQueue[channel]
150+
handlers.forEach(handler => {
151+
this.unsubscribe(channel, handler)
152+
this._removeFromQueue(this.unsubscribeQueue, channel, handler)
153+
})
154+
})
155+
}
156+
_addToSubscribeQueue (channel, handler) {
157+
console.log('[RealTimeClient] Adding channel subscribe to queue. Channel: ' + channel)
158+
// To make sure the unsubscribe won't be sent out to the server
159+
this._removeFromQueue(this.unsubscribeQueue, channel, handler)
160+
const handlers = this.subscribeQueue[channel]
161+
if (!handlers) {
162+
this.subscribeQueue[channel] = [handler]
163+
} else {
164+
handlers.push(handler)
165+
}
166+
}
167+
_addToUnsubscribeQueue (channel, handler) {
168+
console.log('[RealTimeClient] Adding channel unsubscribe to queue. Channel: ' + channel)
169+
// To make sure the subscribe won't be sent out to the server
170+
this._removeFromQueue(this.subscribeQueue, channel, handler)
171+
const handlers = this.unsubscribeQueue[channel]
172+
if (!handlers) {
173+
this.unsubscribeQueue[channel] = [handler]
174+
} else {
175+
handlers.push(handlers)
176+
}
177+
}
178+
_removeFromQueue (queue, channel, handler) {
179+
const handlers = queue[channel]
180+
if (handlers) {
181+
let index = handlers.indexOf(handler)
182+
if (index > -1) {
183+
handlers.splice(index, 1)
184+
}
185+
}
186+
}
187+
}
188+
189+
export default new RealTimeClient()

front-end/src/services/me.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from 'axios'
22
import errorParser from '@/utils/error-parser'
3+
import eventBus from '@/event-bus'
34

45
export default {
56
/**
@@ -9,6 +10,7 @@ export default {
910
return new Promise((resolve, reject) => {
1011
axios.get('/me').then(({data}) => {
1112
resolve(data)
13+
eventBus.$emit('myDataFetched', data)
1214
}).catch((error) => {
1315
reject(errorParser.parse(error))
1416
})

front-end/src/store/actions.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import meService from '@/services/me'
22

3+
export const logout = ({ commit }) => {
4+
commit('logout')
5+
}
6+
37
export const getMyData = ({ commit }) => {
48
meService.getMyData().then(data => {
59
commit('updateMyData', data)

front-end/src/store/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Vue.use(Vuex)
99

1010
const state = {
1111
user: {
12-
name: null
12+
name: null,
13+
authenticated: false
1314
},
1415
teams: [/* {id, name} */],
1516
boards: [/* {id, name, description, teamId} */]

0 commit comments

Comments
 (0)