diff options
Diffstat (limited to 'frontend/src')
-rw-r--r-- | frontend/src/api/sevenWondersApi.js | 66 | ||||
-rw-r--r-- | frontend/src/api/websocket.js | 32 | ||||
-rw-r--r-- | frontend/src/models/games.js | 10 | ||||
-rw-r--r-- | frontend/src/routes.js | 9 | ||||
-rw-r--r-- | frontend/src/sagas.js | 13 | ||||
-rw-r--r-- | frontend/src/sagas/errors.js | 17 | ||||
-rw-r--r-- | frontend/src/sagas/gameBrowser.js | 29 | ||||
-rw-r--r-- | frontend/src/sagas/home.js | 17 | ||||
-rw-r--r-- | frontend/src/sagas/lobby.js | 28 | ||||
-rw-r--r-- | frontend/src/utils/websocket.js | 50 |
10 files changed, 155 insertions, 116 deletions
diff --git a/frontend/src/api/sevenWondersApi.js b/frontend/src/api/sevenWondersApi.js new file mode 100644 index 00000000..4ba0fff3 --- /dev/null +++ b/frontend/src/api/sevenWondersApi.js @@ -0,0 +1,66 @@ +import { createJsonSubscriptionChannel, createStompSession } from './websocket'; +import type { Client } from 'webstomp-client'; +import type { Channel } from 'redux-saga'; + +const wsURL = '/seven-wonders-websocket'; + +export class SevenWondersSession { + client: Client; + + constructor(client: Client) { + this.client = client; + } + + watchErrors(): Channel<ApiError> { + return createJsonSubscriptionChannel(this.client, '/user/queue/errors'); + } + + chooseName(displayName: string): void { + this.client.send('/app/chooseName', JSON.stringify({ playerName: displayName })); + } + + watchNameChoice(): Channel<Object> { + return createJsonSubscriptionChannel(this.client, '/user/queue/nameChoice'); + } + + watchGames(): Channel<Object> { + return createJsonSubscriptionChannel(this.client, '/topic/games'); + } + + watchLobbyJoined(): Channel<Object> { + return createJsonSubscriptionChannel(this.client, '/user/queue/lobby/joined'); + } + + watchLobbyUpdated(currentGameId: number): Channel<Object> { + return createJsonSubscriptionChannel(this.client, `/topic/lobby/${currentGameId}/updated`); + } + + watchGameStarted(currentGameId: number): Channel<Object> { + return createJsonSubscriptionChannel(this.client, `/topic/lobby/${currentGameId}/started`); + } + + createGame(gameName: string): void { + this.client.send('/app/lobby/create', JSON.stringify({ gameName })); + } + + joinGame(gameId: number): void { + this.client.send('/app/lobby/join', JSON.stringify({ gameId })); + } + + startGame(): void { + this.client.send('/app/lobby/startGame', {}); + } +} + +export function createSession(): Promise<SevenWondersSession> { + return createStompSession(wsURL).then(client => new SevenWondersSession(client)); +} + +export class ApiError { + message: string; + details: ApiErrorDetail[]; +} + +export class ApiErrorDetail { + message: string; +} diff --git a/frontend/src/api/websocket.js b/frontend/src/api/websocket.js new file mode 100644 index 00000000..6dc6e1a0 --- /dev/null +++ b/frontend/src/api/websocket.js @@ -0,0 +1,32 @@ +// @flow +import SockJS from 'sockjs-client'; +import Stomp from 'webstomp-client'; +import type { Client, Frame, Subscription } from 'webstomp-client'; + +import { eventChannel } from 'redux-saga'; +import type { Channel } from 'redux-saga'; + +function createStompClient(url: string): Client { + return Stomp.over(new SockJS(url), { + debug: process.env.NODE_ENV !== 'production', + }); +} + +export function createStompSession(url: string, headers: Object = {}): Promise<Client> { + return new Promise((resolve, reject) => { + const client: Client = createStompClient(url); + const onSuccess = (frame: Frame) => resolve(client); + client.connect(headers, onSuccess, reject); + }); +} + +export function createJsonSubscriptionChannel(client: Client, path: string): Channel { + return eventChannel((emitter: (data: any) => void) => { + const socketSubscription: Subscription = client.subscribe(path, (frame: Frame) => { + // not all frames have a JSON body + const value = frame && frame.body && JSON.parse(frame.body); + emitter(value); + }); + return () => socketSubscription.unsubscribe(); + }); +} diff --git a/frontend/src/models/games.js b/frontend/src/models/games.js index aab37aea..36619760 100644 --- a/frontend/src/models/games.js +++ b/frontend/src/models/games.js @@ -8,11 +8,11 @@ export type SettingsShape = { discardedCardGold: number, defaultTradingCost: number, wonPointsPerVictoryPerAge: { - '1': number, - '2': number, - '3': number + "1": number, + "2": number, + "3": number }, - wonderSidePickMethod: 'EACH_RANDOM' | 'TODO', + wonderSidePickMethod: "EACH_RANDOM" | "TODO", pointsPer3Gold: number }; export type SettingsType = Record<SettingsShape>; @@ -39,7 +39,7 @@ export type GameShape = { name: string | void, players: List<string>, settings: SettingsType, - state: 'LOBBY' | 'TODO' + state: "LOBBY" | "TODO" }; export type GameType = Record<GameShape>; export type GameMapType = Map<string, GameShape>; diff --git a/frontend/src/routes.js b/frontend/src/routes.js index 40906b93..52990728 100644 --- a/frontend/src/routes.js +++ b/frontend/src/routes.js @@ -9,16 +9,17 @@ import HomePage from './containers/home'; import GameBrowser from './containers/gameBrowser'; import Lobby from './containers/lobby'; import Error404 from './components/errors/Error404'; +import { SevenWondersSession } from './api/sevenWondersApi'; -export const makeSagaRoutes = wsConnection => ({ +export const makeSagaRoutes = (sevenWondersSession: SevenWondersSession) => ({ *'/'() { - yield fork(homeSaga, wsConnection); + yield fork(homeSaga, sevenWondersSession); }, *'/games'() { - yield fork(gameBrowserSaga, wsConnection); + yield fork(gameBrowserSaga, sevenWondersSession); }, *'/lobby/*'() { - yield fork(lobbySaga, wsConnection); + yield fork(lobbySaga, sevenWondersSession); }, }); diff --git a/frontend/src/sagas.js b/frontend/src/sagas.js index 037aa9ed..2b690a02 100644 --- a/frontend/src/sagas.js +++ b/frontend/src/sagas.js @@ -3,20 +3,19 @@ import { router } from 'redux-saga-router'; import { call, fork } from 'redux-saga/effects'; import { makeSagaRoutes } from './routes'; -import { createWsConnection } from './utils/websocket'; import errorHandlingSaga from './sagas/errors'; -import type { SocketObjectType } from './utils/websocket'; import type { History } from 'react-router'; +import { SevenWondersSession, createSession } from './api/sevenWondersApi'; export default function* rootSaga(history: History): * { - let wsConnection: SocketObjectType | void; + let sevenWondersSession: SevenWondersSession | void; try { - wsConnection = yield call(createWsConnection); + sevenWondersSession = yield call(createSession); } catch (error) { - console.error('Could not connect to socket'); + console.error('Could not connect to socket', error); return; } - yield fork(errorHandlingSaga, wsConnection); - yield* router(history, makeSagaRoutes(wsConnection)); + yield fork(errorHandlingSaga, sevenWondersSession); + yield* router(history, makeSagaRoutes(sevenWondersSession)); } diff --git a/frontend/src/sagas/errors.js b/frontend/src/sagas/errors.js index e43875ae..86fa0124 100644 --- a/frontend/src/sagas/errors.js +++ b/frontend/src/sagas/errors.js @@ -1,12 +1,13 @@ -import { apply, call, cancelled, take } from 'redux-saga/effects'; -import { createSubscriptionChannel } from '../utils/websocket'; -import { toastr } from 'react-redux-toastr'; +import {apply, cancelled, take} from 'redux-saga/effects'; +import {toastr} from 'react-redux-toastr'; +import {ApiError, SevenWondersSession} from '../api/sevenWondersApi'; +import type {Channel} from 'redux-saga'; -export default function* errorHandlingSaga({ socket }) { - const errorChannel = yield call(createSubscriptionChannel, socket, '/user/queue/errors'); +export default function* errorHandlingSaga(session: SevenWondersSession) { + const errorChannel: Channel<ApiError> = yield apply(session, session.watchErrors, []); try { while (true) { - const error = yield take(errorChannel); + const error: ApiError = yield take(errorChannel); yield* handleOneError(error); } } finally { @@ -17,13 +18,13 @@ export default function* errorHandlingSaga({ socket }) { } } -function* handleOneError(err) { +function* handleOneError(err: ApiError) { console.error('Error received on web socket channel', err); const msg = buildMsg(err); yield apply(toastr, toastr.error, [msg, { icon: 'error' }]); } -function buildMsg(err) { +function buildMsg(err: ApiError): string { if (err.details.length > 0) { return err.details.map(d => d.message).join('\n'); } else { diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js index 55927bf2..17eb9287 100644 --- a/frontend/src/sagas/gameBrowser.js +++ b/frontend/src/sagas/gameBrowser.js @@ -1,6 +1,5 @@ // @flow import { call, put, take, apply } from 'redux-saga/effects'; -import { createSubscriptionChannel } from '../utils/websocket'; import { push } from 'react-router-redux'; import { normalize } from 'normalizr'; @@ -8,11 +7,10 @@ import { game as gameSchema, gameList as gameListSchema } from '../schemas/games import { actions as gameActions, types } from '../redux/games'; import { actions as playerActions } from '../redux/players'; +import type { SevenWondersSession } from '../api/sevenWondersApi'; -import type { SocketObjectType, SocketType } from '../utils/websocket'; - -function* watchGames({ socket }: { socket: SocketType }): * { - const gamesChannel = yield call(createSubscriptionChannel, socket, '/topic/games'); +function* watchGames(session: SevenWondersSession): * { + const gamesChannel = yield apply(session, session.watchGames, []); try { while (true) { const gameList = yield take(gamesChannel); @@ -26,8 +24,8 @@ function* watchGames({ socket }: { socket: SocketType }): * { } } -function* watchLobbyJoined({ socket }: { socket: SocketType }): * { - const joinedLobbyChannel = yield call(createSubscriptionChannel, socket, '/user/queue/lobby/joined'); +function* watchLobbyJoined(session: SevenWondersSession): * { + const joinedLobbyChannel = yield apply(session, session.watchLobbyJoined, []); try { const joinedLobby = yield take(joinedLobbyChannel); const normalized = normalize(joinedLobby, gameSchema); @@ -41,27 +39,22 @@ function* watchLobbyJoined({ socket }: { socket: SocketType }): * { } } -function* createGame({ socket }: { socket: SocketType }): * { +function* createGame(session: SevenWondersSession): * { while (true) { const { gameName } = yield take(types.REQUEST_CREATE_GAME); - yield apply(socket, socket.send, ['/app/lobby/create', JSON.stringify({ gameName }), {}]); + yield apply(session, session.createGame, [gameName]); } } -function* joinGame({ socket }: { socket: SocketType }): * { +function* joinGame(session: SevenWondersSession): * { while (true) { const { gameId } = yield take(types.REQUEST_JOIN_GAME); - yield apply(socket, socket.send, ['/app/lobby/join', JSON.stringify({ gameId }), {}]); + yield apply(session, session.joinGame, [gameId]); } } -function* gameBrowserSaga(socketConnection: SocketObjectType): * { - yield [ - call(watchGames, socketConnection), - call(watchLobbyJoined, socketConnection), - call(createGame, socketConnection), - call(joinGame, socketConnection), - ]; +function* gameBrowserSaga(session: SevenWondersSession): * { + yield [call(watchGames, session), call(watchLobbyJoined, session), call(createGame, session), call(joinGame, session)]; } export default gameBrowserSaga; diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js index 579c7fd6..eb65097b 100644 --- a/frontend/src/sagas/home.js +++ b/frontend/src/sagas/home.js @@ -1,18 +1,18 @@ 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'; +import type { SevenWondersSession } from '../api/sevenWondersApi'; -function* sendUsername({ socket }) { +function* sendUsername(session: SevenWondersSession) { while (true) { const { username } = yield take(types.REQUEST_CHOOSE_USERNAME); - yield apply(socket, socket.send, ['/app/chooseName', JSON.stringify({ playerName: username })]); + yield apply(session, session.chooseName, [username]); } } -function* validateUsername({ socket }) { - const usernameChannel = yield call(createSubscriptionChannel, socket, '/user/queue/nameChoice'); +function* validateUsername(session: SevenWondersSession) { + const usernameChannel = yield apply(session, session.watchNameChoice, []); while (true) { const user = yield take(usernameChannel); yield put(actions.setCurrentPlayer(user)); @@ -21,9 +21,8 @@ function* validateUsername({ socket }) { } } -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)]; +function* homeSaga(session: SevenWondersSession) { + yield [call(sendUsername, session), call(validateUsername, session)]; } -export default usernameChoiceSaga; +export default homeSaga; diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js index d11511e8..0c264dde 100644 --- a/frontend/src/sagas/lobby.js +++ b/frontend/src/sagas/lobby.js @@ -1,5 +1,6 @@ import { call, put, take, apply } from 'redux-saga/effects'; -import { createSubscriptionChannel } from '../utils/websocket'; +import type { Channel } from 'redux-saga'; + import { push } from 'react-router-redux'; import { normalize } from 'normalizr'; @@ -7,15 +8,16 @@ import { game as gameSchema } from '../schemas/games'; import { actions as gameActions, types } from '../redux/games'; import { actions as playerActions } from '../redux/players'; +import { SevenWondersSession } from '../api/sevenWondersApi'; -function getCurrentGameId() { +function getCurrentGameId(): number { const path = window.location.pathname; return path.split('lobby/')[1]; } -function* watchLobbyUpdates({ socket }) { - const currentGameId = getCurrentGameId(); - const lobbyUpdatesChannel = yield call(createSubscriptionChannel, socket, `/topic/lobby/${currentGameId}/updated`); +function* watchLobbyUpdates(session: SevenWondersSession) { + const currentGameId: number = getCurrentGameId(); + const lobbyUpdatesChannel: Channel = yield apply(session, session.watchLobbyUpdated, [currentGameId]); try { while (true) { const lobby = yield take(lobbyUpdatesChannel); @@ -28,9 +30,9 @@ function* watchLobbyUpdates({ socket }) { } } -function* watchGameStart({ socket }) { +function* watchGameStart(session: SevenWondersSession) { const currentGameId = getCurrentGameId(); - const gameStartedChannel = yield call(createSubscriptionChannel, socket, `/topic/lobby/${currentGameId}/started`); + const gameStartedChannel = yield apply(session, session.watchGameStarted, [currentGameId]); try { yield take(gameStartedChannel); yield put(gameActions.enterGame()); @@ -40,19 +42,15 @@ function* watchGameStart({ socket }) { } } -function* startGame({ socket }) { +function* startGame(session: SevenWondersSession) { while (true) { yield take(types.REQUEST_START_GAME); - yield apply(socket, socket.send, ['/app/lobby/startGame', {}]); + yield apply(session, session.startGame, []); } } -function* lobbySaga(socketConnection) { - yield [ - call(watchLobbyUpdates, socketConnection), - call(watchGameStart, socketConnection), - call(startGame, socketConnection), - ]; +function* lobbySaga(session: SevenWondersSession) { + yield [call(watchLobbyUpdates, session), call(watchGameStart, session), call(startGame, session)]; } export default lobbySaga; diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js deleted file mode 100644 index 6acd0806..00000000 --- a/frontend/src/utils/websocket.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow -import SockJS from 'sockjs-client'; -import Stomp from 'webstomp-client'; -import { eventChannel } from 'redux-saga'; - -const wsURL = '/seven-wonders-websocket'; - -export type FrameType = { - body: string, - command: string, - header: { - 'heart-beat': number, - 'user-name': string, - version: string - } -}; -export type SocketType = { - connect: (headers: Object, onSucces: (frame: FrameType) => void, onReject: (error: any) => void) => void, - subscribe: (path: string, callback: (event: any) => void) => Object -}; -export type SocketSubscriptionType = { - id: string, - unsubscribe: () => void -}; -export type SocketEventType = { - body: string -}; -export type SocketObjectType = { - frame: FrameType, - socket: SocketType -}; - -export const createWsConnection = (headers: Object = {}): Promise<SocketObjectType> => - new Promise((resolve, reject) => { - let socket: SocketType = Stomp.over(new SockJS(wsURL), { - debug: process.env.NODE_ENV !== 'production', - }); - socket.connect(headers, (frame: FrameType) => resolve({ frame, socket }), reject); - }); - -export const createSubscriptionChannel = (socket: SocketType, path: string) => { - return eventChannel((emitter: (data: any) => void) => { - const socketSubscription: SocketSubscriptionType = socket.subscribe(path, (event: SocketEventType) => { - // not all events have a body - const value = event.body && JSON.parse(event.body); - emitter(value); - }); - return () => socketSubscription.unsubscribe(); - }); -}; |