summaryrefslogtreecommitdiff
path: root/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src')
-rw-r--r--frontend/src/api/sevenWondersApi.js66
-rw-r--r--frontend/src/api/websocket.js32
-rw-r--r--frontend/src/models/games.js10
-rw-r--r--frontend/src/routes.js9
-rw-r--r--frontend/src/sagas.js13
-rw-r--r--frontend/src/sagas/errors.js17
-rw-r--r--frontend/src/sagas/gameBrowser.js29
-rw-r--r--frontend/src/sagas/home.js17
-rw-r--r--frontend/src/sagas/lobby.js28
-rw-r--r--frontend/src/utils/websocket.js50
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();
- });
-};
bgstack15