Parcourir la source

Full client WebSocket support

rigwild il y a 4 ans
Parent
commit
4898cd9cbe

+ 1 - 1
.eslintrc.js

@@ -134,7 +134,7 @@ module.exports = {
     'no-mixed-spaces-and-tabs': 1,
     'no-multiple-empty-lines': [1, { max: 2 }],
     'no-trailing-spaces': 1,
-    'no-underscore-dangle': 1,
+    'no-underscore-dangle': 0,
     'quote-props': [1, 'consistent'],
     'quotes': [1, 'single'],
     'semi': ['error', 'never'],

+ 1 - 0
package.json

@@ -24,6 +24,7 @@
     "serve-static": "^1.13.2",
     "sharp": "^0.22.1",
     "vue": "^2.6.10",
+    "vue-native-websocket": "^2.0.13",
     "vue-router": "^3.0.6",
     "vuetify": "^1.5.14",
     "vuex": "^3.1.0",

+ 1 - 1
server/webSocket/messageHandler.js

@@ -24,7 +24,7 @@ const messageHandler = ws => async data => {
 
   await DataController.add(json)
   if (!TEST_MODE) wsLogger.info(formatLog(json, 'message'))
-  ws.send('ok')
+  ws.send('{"message":"ok"}')
 }
 
 export default messageHandler

+ 16 - 11
src/App.vue

@@ -90,8 +90,6 @@ export default {
       darkMode: true,
       drawer: false,
 
-      hostConfigured: false,
-
       loadingErrorMessage: null,
       loadingMessage: null
     }
@@ -101,24 +99,23 @@ export default {
   },
   watch: {
     isHostConfigured(value) {
-      if (!this.areScenesLoaded && value) this.loadAppData()
+      if (value) this.loadAppData()
     }
   },
   mounted() {
-    if (this.isHostConfigured && !this.areScenesLoaded) this.loadAppData()
+    this.loadAppData()
   },
   methods: {
-    ...mapActions(['loadScenesList']),
+    ...mapActions(['loadScenesList', 'connectToWs']),
     async loadAppData() {
-      if (this.isHostConfigured && !this.areScenesLoaded) {
-        await this.loadScenes()
-      }
+      if (this.isHostConfigured) await this.loadWebSocket()
+      if (this.isHostConfigured && !this.areScenesLoaded) await this.loadScenes()
     },
 
-    async loadScenes() {
-      this.loadingMessage = 'Loading scenes list...'
+    async load(fn, loadingMessage) {
       try {
-        await this.loadScenesList()
+        this.loadingMessage = loadingMessage
+        await fn()
       }
       catch (err) {
         this.loadingErrorMessage = err.message
@@ -127,7 +124,15 @@ export default {
       finally {
         this.loadingMessage = null
       }
+    },
+
+    loadScenes() {
+      return this.load(this.loadScenesList, 'Loading scenes list...')
+    },
+    loadWebSocket() {
+      return this.load(this.connectToWs, 'Connecting to WebSocket server...')
     }
+
   }
 }
 </script>

+ 7 - 7
src/components/HostConfig.vue

@@ -12,9 +12,9 @@
                 <v-form v-else ref="form">
                   <v-flex xs3>
                     <v-select
-                      v-model="config.protocol"
-                      :items="['HTTP', 'HTTPS']"
-                      label="Protocol"
+                      v-model="config.ssl"
+                      :items="[true, false]"
+                      label="SSL"
                     />
                   </v-flex>
 
@@ -63,7 +63,7 @@ export default {
   data() {
     return {
       config: {
-        protocol: 'HTTP',
+        ssl: true,
         host: 'diran.univ-littoral.fr',
         port: '80'
       },
@@ -74,15 +74,15 @@ export default {
   },
 
   watch: {
-    'config.protocol'(newValue) {
-      if (newValue === 'HTTPS') this.config.port = 443
+    'config.ssl'(newValue) {
+      if (newValue === true) this.config.port = 443
     }
   },
 
   methods: {
     ...mapActions(['setHostConfig']),
     reset() {
-      this.config.protocol = 'HTTP'
+      this.config.ssl = true
       this.config.host = ''
       this.config.port = null
       this.configErrorMessage = null

+ 2 - 1
src/functions.js

@@ -20,4 +20,5 @@ export const API_ROUTES = {
 
 export const delay = ms => new Promise(res => setTimeout(res, ms))
 
-export const buildURI = (protocol, host, port, route = '') => `${protocol}://${host}:${port}${route}`
+export const buildURI = (ssl, host, port, route = '') => `${ssl ? 'https' : 'http'}://${host}:${port}${route}`
+export const buildWsURI = (ssl, host, port) => `${ssl ? 'wss' : 'ws'}://${host}:${port}`

+ 10 - 0
src/main.js

@@ -3,9 +3,19 @@ import './plugins/vuetify'
 import App from './App.vue'
 import router from './router'
 import store from './store'
+import VueNativeSock from 'vue-native-websocket'
 
 Vue.config.productionTip = false
 
+Vue.use(VueNativeSock, 'ws://example.com', {
+  store,
+  connectManually: true,
+  reconnection: true,
+  reconnectionAttempts: 5,
+  reconnectionDelay: 1000
+})
+store.$socket = Vue.prototype.$socket
+
 new Vue({
   router,
   store,

+ 19 - 4
src/store/actions.js

@@ -1,17 +1,18 @@
-import { API_ROUTES, buildURI } from '../functions'
+import Vue from 'vue'
+import { API_ROUTES, buildURI, buildWsURI } from '../functions'
 
 export default {
   resetApp({ commit }, { hostConfig = false, progression = false }) {
     commit('resetApp', { hostConfig, progression })
   },
 
-  async setHostConfig({ commit }, { protocol, host, port }) {
+  async setHostConfig({ commit }, { ssl, host, port }) {
     // Timeout after 1s
     const controller = new AbortController()
     const signal = controller.signal
     setTimeout(() => controller.abort(), 1500)
 
-    const URI = buildURI(protocol, host, port, API_ROUTES.ping())
+    const URI = buildURI(ssl, host, port, API_ROUTES.ping())
     return fetch(URI, { signal })
       .then(async res => {
         if (res.status !== 200) throw new Error(`Received wrong HTTP status code : ${res.status} (Need 200).`)
@@ -19,8 +20,10 @@ export default {
         const content = await res.text()
         if (content !== 'pong') throw new Error('Received wrong web content (Need to receive "pong").')
 
+        this._vm.$connect(buildWsURI(ssl, host, port))
+
         // Configuration is valid
-        commit('setHostConfig', { protocol, host, port })
+        commit('setHostConfig', { ssl, host, port })
       })
       .catch(err => {
         // Host not reachable or invalid HTTP status code
@@ -28,6 +31,18 @@ export default {
       })
   },
 
+  async connectToWs({ state, getters }) {
+    if (state.socket.isConnected) return /*eslint-disable-line */
+    else if (getters.isHostConfigured) {
+      this._vm.$connect(getters.getHostWsURI)
+    }
+    else throw new Error('Could not connect to WebSocket server. Host is not configured.')
+  },
+
+  sendMessage(_, message) {
+    Vue.prototype.$socket.send(JSON.stringify(message) || message)
+  },
+
   async loadScenesList({ getters: { isHostConfigured, getHostURI }, commit }) {
     if (!isHostConfigured) throw new Error('Host is not configured.')
 

+ 8 - 3
src/store/getters.js

@@ -1,12 +1,17 @@
-import { buildURI } from '../functions'
+import { buildURI, buildWsURI } from '../functions'
 
 export default {
   isHostConfigured(state) {
-    return !!(state.hostConfig.protocol && state.hostConfig.host && state.hostConfig.protocol)
+    return !!(state.hostConfig.ssl !== null && state.hostConfig.host && state.hostConfig.port)
   },
   getHostURI(state, getters) {
     if (getters.isHostConfigured)
-      return buildURI(state.hostConfig.protocol, state.hostConfig.host, state.hostConfig.port)
+      return buildURI(state.hostConfig.ssl, state.hostConfig.host, state.hostConfig.port)
+  },
+
+  getHostWsURI(state, getters) {
+    if (getters.isHostConfigured)
+      return buildWsURI(state.hostConfig.ssl, state.hostConfig.host, state.hostConfig.port)
   },
 
   areScenesLoaded(state) {

+ 8 - 10
src/store/index.js

@@ -1,29 +1,27 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 import VuexPersistence from 'vuex-persist'
-import state from './state'
+import defaultState from './state'
 import getters from './getters'
 import mutations from './mutations'
 import actions from './actions'
 
 Vue.use(Vuex)
 
-const PRODUCTION_MODE = process.env.NODE_ENV === 'production'
-
 const vuexLocal = new VuexPersistence({
   storage: window.localStorage,
   key: 'webexpe-state',
-  strictMode: !PRODUCTION_MODE
+  reducer: state => ({
+    hostConfig: state.hostConfig,
+    scenesList: state.scenesList,
+    progression: state.progression
+  })
 })
 
 export default new Vuex.Store({
-  state,
+  state: defaultState(),
   getters,
-  mutations: {
-    RESTORE_MUTATION: !PRODUCTION_MODE ? vuexLocal.RESTORE_MUTATION : undefined,
-    ...mutations
-  },
+  mutations,
   actions,
-  strict: !PRODUCTION_MODE,
   plugins: [vuexLocal.plugin]
 })

+ 34 - 7
src/store/mutations.js

@@ -1,3 +1,4 @@
+import Vue from 'vue'
 import { defaultState } from '@/store/state'
 import Experiments from '@/router/experiments'
 
@@ -20,13 +21,16 @@ export default {
 
   setListScenes(state, scenes) {
     state.scenesList = scenes
-    const scenesProgressObj = scenes.reduce((acc, x) => {
-      acc[x] = { done: false, data: {} }
-      return acc
-    }, {})
-    const progressionObj = Experiments.reduce((acc, x) => {
-      acc[x.name] = scenesProgressObj
-      return acc
+    const progressionObj = Experiments.reduce((accExpe, expe) => {
+      const scenesProgressObj = scenes.reduce((accScene, scene) => {
+        // Do not overwrite current progression
+        if (state.progression[expe.name] && state.progression[expe.name][scene])
+          accScene[scene] = state.progression[expe.name][scene]
+        else accScene[scene] = { done: false, data: {} }
+        return accScene
+      }, {})
+      accExpe[expe.name] = scenesProgressObj
+      return accExpe
     }, {})
 
     state.progression = progressionObj
@@ -39,5 +43,28 @@ export default {
   setExperimentDone(state, { experimentName, sceneName, done }) {
     checkProgression(state, experimentName, sceneName)
     state.progression[experimentName][sceneName].done = done
+  },
+
+  SOCKET_ONOPEN(state, event) {
+    Vue.prototype.$socket = event.currentTarget
+    state.socket.isConnected = true
+  },
+  SOCKET_ONCLOSE(state, event) {
+    state.socket.isConnected = false
+  },
+  SOCKET_ONERROR(state, event) {
+    console.error(state, event)
+  },
+  // default handler called for all methods
+  SOCKET_ONMESSAGE(state, { data: rawMessage }) {
+    const message = JSON.parse(rawMessage)
+    state.socket.message = message
+  },
+  // mutations for reconnect methods
+  SOCKET_RECONNECT(state, count) {
+    console.info(state, count)
+  },
+  SOCKET_RECONNECT_ERROR(state) {
+    state.socket.reconnectError = true
   }
 }

+ 10 - 7
src/store/state.js

@@ -1,12 +1,15 @@
-export const defaultState = {
+// Deep copy to not mutate it with the store (default state is needed when reloading after a refresh)
+export default () => JSON.parse(JSON.stringify({
   hostConfig: {
-    protocol: null,
+    ssl: null,
     host: null,
     port: null
   },
   scenesList: null,
-  progression: {}
-}
-
-// Deep copy defaultState to not modify it with the store
-export default JSON.parse(JSON.stringify(defaultState))
+  progression: {},
+  socket: {
+    isConnected: false,
+    message: '',
+    reconnectError: false
+  }
+}))

+ 38 - 2
src/views/Experiments/WithReference.vue

@@ -1,9 +1,45 @@
 <template>
-  <div></div>
+  <div>
+    <v-container grid-list-md text-xs-center fluid>
+      <v-layout row wrap>
+        <v-flex xs6>
+          <v-card dark color="primary">
+            <v-card-text class="px-0">Experience image</v-card-text>
+            <v-img src="https://diran.univ-littoral.fr/api/images/Appart1opt02/appartAopt_00900.png" />
+          </v-card>
+        </v-flex>
+        <v-flex xs6>
+          <v-card dark color="primary">
+            <v-card-text>Reference image</v-card-text>
+            <v-img src="https://diran.univ-littoral.fr/api/images/Appart1opt02/appartAopt_00900.png" />
+          </v-card>
+        </v-flex>
+      </v-layout>
+    </v-container>
+  </div>
 </template>
 
 <script>
+import { mapGetters } from 'vuex'
+import { API_ROUTES } from '@/functions'
+
 export default {
-  name: 'ExperimentWithReference'
+  name: 'ExperimentWithReference',
+  props: {
+    sceneId: {
+      type: String,
+      required: true
+    }
+  },
+  computed: {
+  },
+  async mounted() {
+    await this.getExtracts()
+  },
+  methods: {
+    async getExtracts() {
+      const scenes = await fetch(`${this.getHostURI}${API_ROUTES.getImage()}`).then(res => res.json())
+    }
+  }
 }
 </script>

+ 5 - 0
yarn.lock

@@ -9637,6 +9637,11 @@ vue-loader@^15.7.0:
     vue-hot-reload-api "^2.3.0"
     vue-style-loader "^4.1.0"
 
+vue-native-websocket@^2.0.13:
+  version "2.0.13"
+  resolved "https://registry.yarnpkg.com/vue-native-websocket/-/vue-native-websocket-2.0.13.tgz#5eaba0e7ba08749d7bff331e3290cdf5e61ca918"
+  integrity sha512-w91n76ZcvjCUzWRKX7SkVTqr9YXTxbdYQmf4RX1LvMdsM0RQvpRdWvzTWaY6kw/egsdRKVSceqDsJKbr+WTyMQ==
+
 vue-router@^3.0.6:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.6.tgz#2e4f0f9cbb0b96d0205ab2690cfe588935136ac3"