summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
authorJoffrey BION <joffrey.bion@gmail.com>2017-05-13 22:18:56 +0200
committerJoffrey BION <joffrey.bion@gmail.com>2017-05-13 22:18:56 +0200
commite7606b72118f71097a0a7f75fb735750f905c24a (patch)
tree6cf01487cf2c669e6231c2c16ea3d50093319b0b /frontend/src
parentFix getPlayers that in fact takes an immutable List instead of JS array (diff)
downloadseven-wonders-e7606b72118f71097a0a7f75fb735750f905c24a.tar.gz
seven-wonders-e7606b72118f71097a0a7f75fb735750f905c24a.tar.bz2
seven-wonders-e7606b72118f71097a0a7f75fb735750f905c24a.zip
Migrate to seamless immutable
Resolves: https://github.com/luxons/seven-wonders/issues/6
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/components/gameList.js7
-rw-r--r--frontend/src/components/playerList.js7
-rw-r--r--frontend/src/containers/gameBrowser.js4
-rw-r--r--frontend/src/containers/lobby.js14
-rw-r--r--frontend/src/reducers.js22
-rw-r--r--frontend/src/redux/app.js12
-rw-r--r--frontend/src/redux/errors.js23
-rw-r--r--frontend/src/redux/games.js30
-rw-r--r--frontend/src/redux/players.js23
-rw-r--r--frontend/src/routes.js4
-rw-r--r--frontend/src/sagas.js7
-rw-r--r--frontend/src/sagas/errors.js23
-rw-r--r--frontend/src/sagas/gameBrowser.js33
-rw-r--r--frontend/src/sagas/home.js29
-rw-r--r--frontend/src/sagas/usernameChoice.js41
-rw-r--r--frontend/src/store.js4
-rw-r--r--frontend/src/utils/createWebSocketConnection.js14
-rw-r--r--frontend/src/utils/websocket.js23
18 files changed, 169 insertions, 151 deletions
diff --git a/frontend/src/components/gameList.js b/frontend/src/components/gameList.js
index c8720b26..f49a7589 100644
--- a/frontend/src/components/gameList.js
+++ b/frontend/src/components/gameList.js
@@ -1,15 +1,16 @@
import React from 'react'
import { Flex } from 'reflexbox'
import { Text, Space, Button } from 'rebass'
+import Immutable from 'seamless-immutable'
const GameList = (props) => (
<div>
- {props.games.map((game, index) => {
+ {Immutable.asMutable(props.games).map((game, index) => {
- const joinGame = () => props.joinGame(game.get('id'))
+ const joinGame = () => props.joinGame(game.id)
return (<Flex key={index}>
- <Text>{game.get('name')}</Text>
+ <Text>{game.name}</Text>
<Space auto />
<Button onClick={joinGame}>Join</Button>
</Flex>)
diff --git a/frontend/src/components/playerList.js b/frontend/src/components/playerList.js
index 30384d53..45aa01a2 100644
--- a/frontend/src/components/playerList.js
+++ b/frontend/src/components/playerList.js
@@ -1,13 +1,14 @@
import React from 'react'
import { Flex } from 'reflexbox'
import { Text } from 'rebass'
+import Immutable from 'seamless-immutable'
const PlayerList = (props) => (
<div>
- {props.players.map((player, index) => {
+ {Immutable.asMutable(props.players).map((player, index) => {
return (<Flex key={index}>
- <Text>{player.get('displayName')}</Text>
- <Text>({player.get('username')})</Text>
+ <Text>{player.displayName}</Text>
+ <Text>({player.username})</Text>
</Flex>)
})}
</div>
diff --git a/frontend/src/containers/gameBrowser.js b/frontend/src/containers/gameBrowser.js
index 6a6b3ce2..5db3c1cb 100644
--- a/frontend/src/containers/gameBrowser.js
+++ b/frontend/src/containers/gameBrowser.js
@@ -30,7 +30,7 @@ class GameBrowser extends Component {
>
</InlineForm>
<Space auto />
- <Text><b>Username:</b> {this.props.currentPlayer.get('displayName')}</Text>
+ <Text><b>Username:</b> {this.props.currentPlayer && this.props.currentPlayer.displayName}</Text>
<Space x={1} />
</Flex>
<GameList games={this.props.games} joinGame={this.props.joinGame}/>
@@ -43,7 +43,7 @@ import { getCurrentPlayer } from '../redux/players'
import { getAllGames, actions } from '../redux/games'
const mapStateToProps = (state) => ({
- currentPlayer: getCurrentPlayer(state),
+ currentPlayer: getCurrentPlayer(state) || {displayName: '[ERROR]'},
games: getAllGames(state)
})
diff --git a/frontend/src/containers/lobby.js b/frontend/src/containers/lobby.js
index c36c3263..f0df0c44 100644
--- a/frontend/src/containers/lobby.js
+++ b/frontend/src/containers/lobby.js
@@ -1,15 +1,23 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
-import { List } from 'immutable'
+import Immutable from 'seamless-immutable'
import { Text } from 'rebass'
import PlayerList from '../components/playerList'
class Lobby extends Component {
+ getTitle() {
+ if (this.props.currentGame) {
+ return this.props.currentGame.name
+ } else {
+ return 'What are you doing here? You haven\'t joined a game yet!'
+ }
+ }
+
render() {
return (
<div>
- {this.props.currentGame && <Text>{this.props.currentGame.name}</Text>}
+ <Text>{this.getTitle()}</Text>
<PlayerList players={this.props.players}/>
</div>
)
@@ -23,7 +31,7 @@ const mapStateToProps = (state) => {
const game = getCurrentGame(state)
return ({
currentGame: game,
- players: game ? getPlayers(state, game.get('players')) : List()
+ players: game ? getPlayers(state, game.players) : Immutable([])
})
}
diff --git a/frontend/src/reducers.js b/frontend/src/reducers.js
index 097f9243..7e5fba26 100644
--- a/frontend/src/reducers.js
+++ b/frontend/src/reducers.js
@@ -1,28 +1,14 @@
-import { combineReducers } from 'redux-immutable'
-
-// react-router-redux immutable reducer
-import { fromJS } from 'immutable'
-import { LOCATION_CHANGE } from 'react-router-redux'
-
-const initialState = fromJS({
- locationBeforeTransitions: null
-})
-
-const routerImmutableReducer = (state = initialState, action) => {
- if (action.type === LOCATION_CHANGE) {
- return state.set('locationBeforeTransitions', action.payload)
- }
-
- return state
-}
+import { combineReducers, routerReducer } from 'redux-seamless-immutable'
+import errorsReducer from './redux/errors'
import gamesReducer from './redux/games'
import playersReducer from './redux/players'
export default function createReducer() {
return combineReducers({
+ errors: errorsReducer,
games: gamesReducer,
- routing: routerImmutableReducer,
players: playersReducer,
+ routing: routerReducer,
})
}
diff --git a/frontend/src/redux/app.js b/frontend/src/redux/app.js
index 172dc960..251b12a2 100644
--- a/frontend/src/redux/app.js
+++ b/frontend/src/redux/app.js
@@ -1,15 +1,5 @@
export const makeSelectLocationState = () => {
- let prevRoutingState;
- let prevRoutingStateJS;
-
return (state) => {
- const routingState = state.get('routing')
-
- if (!routingState.equals(prevRoutingState)) {
- prevRoutingState = routingState
- prevRoutingStateJS = routingState.toJS()
- }
-
- return prevRoutingStateJS;
+ return state.routing
}
}
diff --git a/frontend/src/redux/errors.js b/frontend/src/redux/errors.js
new file mode 100644
index 00000000..1c247955
--- /dev/null
+++ b/frontend/src/redux/errors.js
@@ -0,0 +1,23 @@
+import Immutable from 'seamless-immutable'
+
+export const types = {
+ ERROR_RECEIVED_ON_WS: 'ERROR/RECEIVED_ON_WS',
+}
+
+export const actions = {
+ errorReceived: (error) => ({
+ type: types.ERROR_RECEIVED_ON_WS,
+ error
+ })
+}
+
+const initialState = Immutable.from([])
+
+export default (state = initialState, action) => {
+ switch (action.type) {
+ case types.ERROR_RECEIVED_ON_WS:
+ return state.concat([ action.error ])
+ default:
+ return state
+ }
+}
diff --git a/frontend/src/redux/games.js b/frontend/src/redux/games.js
index e78e220f..2b9a92a4 100644
--- a/frontend/src/redux/games.js
+++ b/frontend/src/redux/games.js
@@ -1,4 +1,4 @@
-import {fromJS} from 'immutable'
+import Immutable from 'seamless-immutable'
export const types = {
UPDATE_GAMES: 'GAME/UPDATE_GAMES',
@@ -8,13 +8,13 @@ export const types = {
}
export const actions = {
- updateGames: (games) => ({ type: types.UPDATE_GAMES, games }),
- requestJoinGame: (id) => ({ type: types.REQUEST_JOIN_GAME, id }),
- requestCreateGame: (name) => ({ type: types.REQUEST_CREATE_GAME, name }),
- enterLobby: (lobby) => ({ type: types.ENTER_LOBBY, lobby }),
+ updateGames: (games) => ({ type: types.UPDATE_GAMES, games: Immutable(games) }),
+ requestJoinGame: (gameId) => ({ type: types.REQUEST_JOIN_GAME, gameId }),
+ requestCreateGame: (gameName) => ({ type: types.REQUEST_CREATE_GAME, gameName }),
+ enterLobby: (lobby) => ({ type: types.ENTER_LOBBY, lobby: Immutable(lobby) }),
}
-const initialState = fromJS({
+const initialState = Immutable.from({
all: {},
current: ''
})
@@ -22,18 +22,18 @@ const initialState = fromJS({
export default (state = initialState, action) => {
switch (action.type) {
case types.UPDATE_GAMES:
- return state.setIn(['all'], state.get('all').mergeDeep(action.games))
+ return Immutable.merge(state, {all: action.games}, {deep: true})
case types.ENTER_LOBBY:
- return state.set('current', action.lobby.get('id'))
+ return state.set('current', action.lobby.id)
default:
return state
}
}
-const getState = globalState => globalState.get('games')
-
-export const getAllGamesById = globalState => getState(globalState).get('all')
-export const getAllGames = globalState => getAllGamesById(globalState).toList()
-export const getGame = (globalState, id) => getAllGamesById(globalState).get(String(id))
-export const getCurrentGameId = globalState => getState(globalState).get('current')
-export const getCurrentGame = globalState => getGame(globalState, getCurrentGameId(globalState))
+export const getAllGamesById = state => state.games.all
+export const getAllGames = state => {
+ let gamesById = getAllGamesById(state)
+ return Object.keys(gamesById).map(k => gamesById[k]);
+}
+export const getGame = (state, id) => getAllGamesById(state)[id]
+export const getCurrentGame = state => getGame(state, state.games.current)
diff --git a/frontend/src/redux/players.js b/frontend/src/redux/players.js
index 09f7390b..84e24796 100644
--- a/frontend/src/redux/players.js
+++ b/frontend/src/redux/players.js
@@ -1,4 +1,4 @@
-import { fromJS, Map } from 'immutable'
+import Immutable from 'seamless-immutable'
export const types = {
REQUEST_CHOOSE_USERNAME: 'USER/REQUEST_CHOOSE_USERNAME',
@@ -21,7 +21,7 @@ export const actions = {
}),
}
-const initialState = fromJS({
+const initialState = Immutable.from({
all: {},
current: ''
})
@@ -30,22 +30,15 @@ export default (state = initialState, action) => {
switch (action.type) {
case types.SET_CURRENT_PLAYER:
const player = action.player
- const username = player.get('username')
- return state.setIn(['all', username], player).set('current', username)
+ const withNewPlayer = state.setIn(['all', player.username], player)
+ return Immutable.set(withNewPlayer, 'current', player.username)
case types.UPDATE_PLAYERS:
- return state.setIn(['all'], state.get('all').mergeDeep(action.players))
+ return Immutable.merge(state, {all: action.players}, {deep: true})
default:
return state
}
}
-const getState = globalState => globalState.get('players')
-
-export const getAllPlayersByUsername = globalState => getState(globalState).get('all')
-export const getAllPlayers = globalState => getAllPlayersByUsername(globalState).toList()
-export const getPlayers = (globalState, usernames) => getAllPlayersByUsername(globalState)
- .filter((v, k) => usernames.contains(k))
- .toList()
-export const getCurrentPlayerUsername = globalState => getState(globalState).get('current')
-export const getCurrentPlayer = globalState => getAllPlayersByUsername(globalState)
- .get(getCurrentPlayerUsername(globalState), Map())
+export const getCurrentPlayer = state => state.players.all && state.players.all[state.players.current]
+export const getPlayers = (state, usernames) => Object.values(state.players.all)
+ .filter(p => usernames.indexOf(p.username) !== -1)
diff --git a/frontend/src/routes.js b/frontend/src/routes.js
index 16800736..1ce46bb3 100644
--- a/frontend/src/routes.js
+++ b/frontend/src/routes.js
@@ -1,10 +1,10 @@
import { fork } from 'redux-saga/effects'
-import usernameChoiceSaga from './sagas/usernameChoice'
+import homeSaga from './sagas/home'
import gameBrowserSaga from './sagas/gameBrowser'
export const makeSagaRoutes = wsConnection => ({
*'/'() {
- yield fork(usernameChoiceSaga, wsConnection)
+ yield fork(homeSaga, wsConnection)
},
*'/games'() {
yield fork(gameBrowserSaga, wsConnection)
diff --git a/frontend/src/sagas.js b/frontend/src/sagas.js
index df49b099..c5166cd5 100644
--- a/frontend/src/sagas.js
+++ b/frontend/src/sagas.js
@@ -1,8 +1,9 @@
import { router } from 'redux-saga-router'
-import { call } from 'redux-saga/effects'
+import { call, fork } from 'redux-saga/effects'
import { makeSagaRoutes } from './routes'
-import createWsConnection from './utils/createWebSocketConnection'
+import { createWsConnection } from './utils/websocket'
+import errorHandlingSaga from './sagas/errors'
export default function *rootSaga(history) {
let wsConnection
@@ -12,6 +13,6 @@ export default function *rootSaga(history) {
console.error('Could not connect to socket')
return
}
-
+ yield fork(errorHandlingSaga, wsConnection)
yield* router(history, makeSagaRoutes(wsConnection))
}
diff --git a/frontend/src/sagas/errors.js b/frontend/src/sagas/errors.js
new file mode 100644
index 00000000..6d4df93d
--- /dev/null
+++ b/frontend/src/sagas/errors.js
@@ -0,0 +1,23 @@
+import { apply, call, cancelled, put, take } from 'redux-saga/effects'
+
+import { createSubscriptionChannel } from '../utils/websocket'
+import { actions } from '../redux/errors'
+
+export default function *errorHandlingSaga({ socket }) {
+ const errorChannel = yield call(createSubscriptionChannel, socket, '/user/queue/error')
+ try {
+ while (true) {
+ const error = yield take(errorChannel)
+ yield* handleOneError(error)
+ }
+ } finally {
+ if (yield cancelled()) {
+ yield apply(errorChannel, errorChannel.close)
+ }
+ }
+}
+
+function *handleOneError(error) {
+ console.error("Error received on web socket channel", error)
+ yield put(actions.errorReceived(error))
+}
diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js
index 596da428..4f3309c3 100644
--- a/frontend/src/sagas/gameBrowser.js
+++ b/frontend/src/sagas/gameBrowser.js
@@ -1,6 +1,5 @@
import { call, put, take, apply } from 'redux-saga/effects'
-import { eventChannel} from 'redux-saga'
-import { fromJS } from 'immutable'
+import { eventChannel } from 'redux-saga'
import { push } from 'react-router-redux'
import { normalize } from 'normalizr'
@@ -14,7 +13,7 @@ function gameBrowserChannel(socket) {
const makeHandler = type => event => {
const response = JSON.parse(event.body)
- emit({ type, response })
+ emit({type, response})
}
const newGame = socket.subscribe('/topic/games', makeHandler(types.UPDATE_GAMES))
@@ -27,22 +26,22 @@ function gameBrowserChannel(socket) {
})
}
-export function *watchGames({ socket }) {
+export function *watchGames({socket}) {
const socketChannel = gameBrowserChannel(socket)
try {
while (true) {
- const { type, response } = yield take(socketChannel)
+ const {type, response} = yield take(socketChannel)
switch (type) {
case types.UPDATE_GAMES:
const normGameList = normalize(response, gameList)
- yield put(playerActions.updatePlayers(fromJS(normGameList.entities.players)))
- yield put(gameActions.updateGames(fromJS(normGameList.entities.games)))
+ yield put(playerActions.updatePlayers(normGameList.entities.players || {}))
+ yield put(gameActions.updateGames(normGameList.entities.games || {}))
break
case types.ENTER_LOBBY:
const normGame = normalize(response, game)
- yield put(gameActions.enterLobby(fromJS(normGame.entities.games[normGame.result])))
+ yield put(gameActions.enterLobby(normGame.entities.games[normGame.result]))
socketChannel.close()
yield put(push('/lobby'))
break
@@ -55,24 +54,20 @@ export function *watchGames({ socket }) {
}
}
-export function *createGame({ socket }) {
- const { name } = yield take(types.REQUEST_CREATE_GAME)
+export function *createGame({socket}) {
+ const {gameName} = yield take(types.REQUEST_CREATE_GAME)
- yield apply(socket, socket.send, ["/app/lobby/create", JSON.stringify({ gameName: name }), {}])
+ yield apply(socket, socket.send, ['/app/lobby/create', JSON.stringify({gameName}), {}])
}
-export function *joinGame({ socket }) {
- const { id } = yield take(types.REQUEST_JOIN_GAME)
+export function *joinGame({socket}) {
+ const {gameId} = yield take(types.REQUEST_JOIN_GAME)
- yield apply(socket, socket.send, ["/app/lobby/join", JSON.stringify({ gameId: id }), {}])
+ yield apply(socket, socket.send, ['/app/lobby/join', JSON.stringify({gameId}), {}])
}
export function *gameBrowserSaga(socketConnection) {
- yield [
- call(watchGames, socketConnection),
- call(createGame, socketConnection),
- call(joinGame, socketConnection)
- ]
+ yield [call(watchGames, socketConnection), call(createGame, socketConnection), call(joinGame, socketConnection)]
}
export default gameBrowserSaga
diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js
new file mode 100644
index 00000000..99e6f954
--- /dev/null
+++ b/frontend/src/sagas/home.js
@@ -0,0 +1,29 @@
+import { apply, call, put, take } from 'redux-saga/effects'
+import { createSubscriptionChannel } from '../utils/websocket'
+import { push } from 'react-router-redux'
+
+import { actions, types } from '../redux/players'
+
+function *sendUsername({ socket }) {
+ const {username} = yield take(types.REQUEST_CHOOSE_USERNAME)
+
+ yield apply(socket, socket.send, ['/app/chooseName', JSON.stringify({ playerName: username })])
+}
+
+function *validateUsername({ socket }) {
+ const usernameChannel = yield call(createSubscriptionChannel, socket, '/user/queue/nameChoice')
+ const user = yield take(usernameChannel)
+ yield put(actions.setCurrentPlayer(user))
+ yield apply(usernameChannel, usernameChannel.close)
+ yield put(push('/games'))
+}
+
+function *usernameChoiceSaga(wsConnection) {
+ // TODO: Run sendUsername in loop when we have the ability to cancel saga on route change
+ yield [
+ call(sendUsername, wsConnection),
+ call(validateUsername, wsConnection),
+ ]
+}
+
+export default usernameChoiceSaga
diff --git a/frontend/src/sagas/usernameChoice.js b/frontend/src/sagas/usernameChoice.js
deleted file mode 100644
index ad5b5341..00000000
--- a/frontend/src/sagas/usernameChoice.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { call, take, put } from 'redux-saga/effects'
-import { eventChannel } from 'redux-saga'
-import { push } from 'react-router-redux'
-import { fromJS } from 'immutable'
-
-import { actions, types } from '../redux/players'
-
-function usernameValidationChannel(socket) {
- return eventChannel(emitter => {
- const receiveUsernameHandler = socket.subscribe('/user/queue/nameChoice', event => {
- emitter(fromJS(JSON.parse(event.body)))
- })
- return () => receiveUsernameHandler.unsubscribe()
- })
-}
-
-function *usernameValidation({ socket }) {
- const usernameChannel = yield call(usernameValidationChannel, socket)
- const user = yield take(usernameChannel)
- yield put(actions.setCurrentPlayer(user))
- usernameChannel.close()
- yield put(push('/games'))
-}
-
-function *sendUsername({ socket }) {
- const { username } = yield take(types.REQUEST_CHOOSE_USERNAME)
-
- yield socket.send('/app/chooseName', JSON.stringify({
- playerName: username
- }))
-}
-
-function *usernameChoiceSaga(wsConnection) {
- // TODO: Run sendUsername in loop when we have the ability to cancel saga on route change
- yield [
- call(sendUsername, wsConnection),
- call(usernameValidation, wsConnection),
- ]
-}
-
-export default usernameChoiceSaga
diff --git a/frontend/src/store.js b/frontend/src/store.js
index 2000d706..bd05e2c5 100644
--- a/frontend/src/store.js
+++ b/frontend/src/store.js
@@ -1,7 +1,7 @@
import { createStore, applyMiddleware, compose } from 'redux'
import { browserHistory } from 'react-router'
import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'
-import { fromJS } from 'immutable'
+import Immutable from 'seamless-immutable'
import createReducer from './reducers'
import createSagaMiddleware from 'redux-saga'
@@ -28,7 +28,7 @@ export default function configureStore(initialState = {}) {
const store = createStore(
createReducer(),
- fromJS(initialState),
+ Immutable.from(initialState),
composeEnhancers(...enhancers)
)
diff --git a/frontend/src/utils/createWebSocketConnection.js b/frontend/src/utils/createWebSocketConnection.js
deleted file mode 100644
index f5d29f37..00000000
--- a/frontend/src/utils/createWebSocketConnection.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import SockJS from 'sockjs-client'
-import Stomp from 'webstomp-client'
-const wsURL = '/seven-wonders-websocket'
-
-const createConnection = (headers = {}) => new Promise((resolve, reject) => {
- let socket = Stomp.over(new SockJS(wsURL), {
- debug: process.env.NODE_ENV !== "production"
- })
- socket.connect(headers, (frame) => {
- return resolve({ frame, socket })
- }, reject)
-})
-
-export default createConnection
diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js
new file mode 100644
index 00000000..1dde5397
--- /dev/null
+++ b/frontend/src/utils/websocket.js
@@ -0,0 +1,23 @@
+import SockJS from 'sockjs-client'
+import Stomp from 'webstomp-client'
+import { eventChannel } from 'redux-saga'
+
+const wsURL = '/seven-wonders-websocket'
+
+export const createWsConnection = (headers = {}) => new Promise((resolve, reject) => {
+ let socket = Stomp.over(new SockJS(wsURL), {
+ debug: process.env.NODE_ENV !== "production"
+ })
+ socket.connect(headers, (frame) => {
+ return resolve({ frame, socket })
+ }, reject)
+})
+
+export const createSubscriptionChannel = (socket, path) => {
+ return eventChannel(emitter => {
+ const receiveUsernameHandler = socket.subscribe(path, event => {
+ emitter(JSON.parse(event.body))
+ })
+ return () => receiveUsernameHandler.unsubscribe()
+ })
+}
bgstack15