From 324b51049acac27ea3e7f71acad47aa729dfd8db Mon Sep 17 00:00:00 2001 From: Joffrey BION Date: Mon, 24 Jul 2017 23:51:43 +0200 Subject: Improve sevenWondersApi and types --- frontend/src/api/model.js | 31 ++++++++++++++----------- frontend/src/api/sevenWondersApi.js | 46 +++++++++++++++++++------------------ frontend/src/api/websocket.js | 24 ++++++++++++++----- frontend/src/sagas/errors.js | 17 ++++++++------ frontend/src/sagas/gameBrowser.js | 21 ++++++++--------- frontend/src/sagas/home.js | 22 ++++++++++-------- frontend/src/sagas/lobby.js | 35 ++++++++++++++-------------- frontend/src/sagas/utils.js | 10 -------- 8 files changed, 108 insertions(+), 98 deletions(-) delete mode 100644 frontend/src/sagas/utils.js (limited to 'frontend') diff --git a/frontend/src/api/model.js b/frontend/src/api/model.js index eb7ad9c5..2f9fad37 100644 --- a/frontend/src/api/model.js +++ b/frontend/src/api/model.js @@ -1,15 +1,16 @@ -export class ApiError { +// @flow +export type ApiError = { message: string; details: ApiErrorDetail[]; } -export class ApiErrorDetail { +export type ApiErrorDetail = { message: string; } export type ApiGameState = "LOBBY" | "PLAYING"; -export class ApiLobby { +export type ApiLobby = { id: number; name: string; owner: ApiPlayer; @@ -20,7 +21,7 @@ export class ApiLobby { export type ApiWonderSidePickMethod = "EACH_RANDOM" | "ALL_A" | "ALL_B" | "SAME_RANDOM_FOR_ALL"; -export class ApiSettings { +export type ApiSettings = { randomSeedForTests: number; timeLimitInSeconds: number; wonderSidePickMethod: ApiWonderSidePickMethod; @@ -32,26 +33,28 @@ export class ApiSettings { wonPointsPerVictoryPerAge: Map; } -export class ApiPlayer { +export type ApiPlayer = { username: string; displayName: string; index: number; ready: boolean; } -export class ApiTable {} +export type ApiTable = {} -export class ApiHandCard {} +export type ApiAction = {} -export class ApiCard {} +export type ApiHandCard = {} -export class ApiPreparedCard {} +export type ApiCard = {} -export class ApiPlayerTurnInfo { +export type ApiPreparedCard = {} + +export type ApiPlayerTurnInfo = { playerIndex: number; table: ApiTable; currentAge: number; - action: Action; + action: ApiAction; hand: ApiHandCard[]; neighbourGuildCards: ApiCard[]; message: string; @@ -61,15 +64,15 @@ export type ApiMoveType = "PLAY" | "PLAY_FREE" | "UPGRADE_WONDER" | "DISCARD" | export type ApiProvider = "LEFT_NEIGHBOUR" | "RIGHT_NEIGHBOUR"; export type ApiResourceType = "WOOD" | "STONE" | "ORE" | "CLAY" | "GLASS" | "PAPYRUS" | "LOOM"; -export class ApiResources { +export type ApiResources = { quantities: Map; } -export class ApiBoughtResources { +export type ApiBoughtResources = { provider: ApiProvider; resources: ApiResources; } -export class ApiPlayerMove { +export type ApiPlayerMove = { type: ApiMoveType; cardName: string; boughtResources: ApiBoughtResources[]; diff --git a/frontend/src/api/sevenWondersApi.js b/frontend/src/api/sevenWondersApi.js index 1004c81e..9a68ec66 100644 --- a/frontend/src/api/sevenWondersApi.js +++ b/frontend/src/api/sevenWondersApi.js @@ -1,5 +1,7 @@ -import { createJsonStompClient, JsonStompClient, Callback } from './websocket'; -import { ApiError, ApiLobby, ApiPlayer, ApiPlayerMove, ApiPlayerTurnInfo, ApiPreparedCard, ApiTable } from './model'; +// @flow +import { createJsonStompClient } from './websocket'; +import type { JsonStompClient, SubscribeFn } from './websocket' +import type { ApiError, ApiLobby, ApiPlayer, ApiPlayerMove, ApiPlayerTurnInfo, ApiPreparedCard, ApiTable } from './model'; const wsURL = '/seven-wonders-websocket'; @@ -10,32 +12,32 @@ export class SevenWondersSession { this.client = client; } - watchErrors(callback: Callback): void { - return this.client.subscribe('/user/queue/errors', callback); + watchErrors(): SubscribeFn { + return this.client.subscriber('/user/queue/errors'); } chooseName(displayName: string): void { this.client.send('/app/chooseName', { playerName: displayName }); } - watchNameChoice(callback: Callback): void { - return this.client.subscribe('/user/queue/nameChoice', callback); + watchNameChoice(): SubscribeFn { + return this.client.subscriber('/user/queue/nameChoice'); } - watchGames(callback: Callback): void { - return this.client.subscribe('/topic/games', callback); + watchGames(): SubscribeFn { + return this.client.subscriber('/topic/games'); } - watchLobbyJoined(callback: Callback): void { - return this.client.subscribe('/user/queue/lobby/joined', callback); + watchLobbyJoined(): SubscribeFn { + return this.client.subscriber('/user/queue/lobby/joined'); } - watchLobbyUpdated(currentGameId: number, callback: Callback): void { - return this.client.subscribe(`/topic/lobby/${currentGameId}/updated`, callback); + watchLobbyUpdated(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/lobby/${currentGameId}/updated`); } - watchGameStarted(currentGameId: number, callback: Callback): void { - return this.client.subscribe(`/topic/lobby/${currentGameId}/started`, callback); + watchGameStarted(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/lobby/${currentGameId}/started`); } createGame(gameName: string): void { @@ -50,20 +52,20 @@ export class SevenWondersSession { this.client.send('/app/lobby/startGame'); } - watchPlayerReady(currentGameId: number, callback: Callback): void { - return this.client.subscribe(`/topic/game/${currentGameId}/playerReady`, callback); + watchPlayerReady(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/playerReady`); } - watchTableUpdates(currentGameId: number, callback: Callback): void { - return this.client.subscribe(`/topic/game/${currentGameId}/tableUpdates`, callback); + watchTableUpdates(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/tableUpdates`); } - watchPreparedMove(currentGameId: number, callback: Callback): void { - return this.client.subscribe(`/topic/game/${currentGameId}/prepared`, callback); + watchPreparedMove(currentGameId: number): SubscribeFn { + return this.client.subscriber(`/topic/game/${currentGameId}/prepared`); } - watchTurnInfo(callback: Callback): void { - return this.client.subscribe('/user/queue/game/turnInfo', callback); + watchTurnInfo(): SubscribeFn { + return this.client.subscriber('/user/queue/game/turnInfo'); } sayReady(): void { diff --git a/frontend/src/api/websocket.js b/frontend/src/api/websocket.js index d411587a..8a893a87 100644 --- a/frontend/src/api/websocket.js +++ b/frontend/src/api/websocket.js @@ -7,9 +7,9 @@ const DEFAULT_DEBUG_OPTIONS = { debug: process.env.NODE_ENV !== 'production', }; -export type Callback = (value: T) => void; - -export type Callable = () => void; +export type Callback = (value?: T) => void; +export type UnsubscribeFn = () => void; +export type SubscribeFn = (callback: Callback) => UnsubscribeFn; export class JsonStompClient { client: Client; @@ -24,16 +24,28 @@ export class JsonStompClient { }); } - subscribe(path: string, callback: Callback): Callable { + subscribe(path: string, callback: Callback): UnsubscribeFn { const socketSubscription: Subscription = this.client.subscribe(path, (frame: Frame) => { // not all frames have a JSON body - const value = frame && frame.body && JSON.parse(frame.body); + const value: T = frame && JsonStompClient.parseBody(frame); callback(value); }); return () => socketSubscription.unsubscribe(); } - send(url: string, body: Object) { + static parseBody(frame: Frame): T | void { + try { + return frame.body && JSON.parse(frame.body); + } catch (jsonParseError) { + throw new Error('Cannot parse websocket message as JSON: ' + jsonParseError.message); + } + } + + subscriber(path: string): SubscribeFn { + return (callback: Callback) => this.subscribe(path, callback); + } + + send(url: string, body?: Object) { const strBody = body ? JSON.stringify(body) : ''; this.client.send(url, strBody); } diff --git a/frontend/src/sagas/errors.js b/frontend/src/sagas/errors.js index 86fa0124..eece98c8 100644 --- a/frontend/src/sagas/errors.js +++ b/frontend/src/sagas/errors.js @@ -1,10 +1,13 @@ -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'; +// @flow +import { toastr } from 'react-redux-toastr' +import type { Channel } from 'redux-saga' +import { eventChannel } from 'redux-saga' +import { apply, cancelled, take } from 'redux-saga/effects' +import type { ApiError } from '../api/model' +import type { SevenWondersSession } from '../api/sevenWondersApi' -export default function* errorHandlingSaga(session: SevenWondersSession) { - const errorChannel: Channel = yield apply(session, session.watchErrors, []); +export default function* errorHandlingSaga(session: SevenWondersSession): * { + const errorChannel: Channel = yield eventChannel(session.watchErrors()); try { while (true) { const error: ApiError = yield take(errorChannel); @@ -18,7 +21,7 @@ export default function* errorHandlingSaga(session: SevenWondersSession) { } } -function* handleOneError(err: ApiError) { +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' }]); diff --git a/frontend/src/sagas/gameBrowser.js b/frontend/src/sagas/gameBrowser.js index 871908f6..5b815f8d 100644 --- a/frontend/src/sagas/gameBrowser.js +++ b/frontend/src/sagas/gameBrowser.js @@ -1,17 +1,16 @@ // @flow -import { call, put, take, apply } from 'redux-saga/effects'; -import { push } from 'react-router-redux'; +import { normalize } from 'normalizr' +import { push } from 'react-router-redux' +import { eventChannel } from 'redux-saga' +import { apply, call, put, take } from 'redux-saga/effects' +import type { SevenWondersSession } from '../api/sevenWondersApi' -import { normalize } from 'normalizr'; -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 { createChannel } from './utils'; +import { actions as gameActions, types } from '../redux/games' +import { actions as playerActions } from '../redux/players' +import { game as gameSchema, gameList as gameListSchema } from '../schemas/games' function* watchGames(session: SevenWondersSession): * { - const gamesChannel = yield createChannel(session, session.watchGames); + const gamesChannel = yield eventChannel(session.watchGames()); try { while (true) { const gameList = yield take(gamesChannel); @@ -26,7 +25,7 @@ function* watchGames(session: SevenWondersSession): * { } function* watchLobbyJoined(session: SevenWondersSession): * { - const joinedLobbyChannel = yield createChannel(session, session.watchLobbyJoined); + const joinedLobbyChannel = yield eventChannel(session.watchLobbyJoined()); try { const joinedLobby = yield take(joinedLobbyChannel); const normalized = normalize(joinedLobby, gameSchema); diff --git a/frontend/src/sagas/home.js b/frontend/src/sagas/home.js index b51bf4dc..0b30f784 100644 --- a/frontend/src/sagas/home.js +++ b/frontend/src/sagas/home.js @@ -1,28 +1,30 @@ -import { apply, call, put, take } from 'redux-saga/effects'; -import { push } from 'react-router-redux'; +// @flow +import { apply, call, put, take } from 'redux-saga/effects' +import { push } from 'react-router-redux' +import { eventChannel } from 'redux-saga' -import { actions, types } from '../redux/players'; -import type { SevenWondersSession } from '../api/sevenWondersApi'; -import { createChannel } from './utils'; +import { actions, types } from '../redux/players' +import type { SevenWondersSession } from '../api/sevenWondersApi' +import type { ApiPlayer } from '../api/model' -function* sendUsername(session: SevenWondersSession) { +function* sendUsername(session: SevenWondersSession): * { while (true) { const { username } = yield take(types.REQUEST_CHOOSE_USERNAME); yield apply(session, session.chooseName, [username]); } } -function* validateUsername(session: SevenWondersSession) { - const usernameChannel = yield createChannel(session, session.watchNameChoice); +function* validateUsername(session: SevenWondersSession): * { + const usernameChannel = yield eventChannel(session.watchNameChoice()); while (true) { - const user = yield take(usernameChannel); + const user: ApiPlayer = yield take(usernameChannel); yield put(actions.setCurrentPlayer(user)); yield apply(usernameChannel, usernameChannel.close); yield put(push('/games')); } } -function* homeSaga(session: SevenWondersSession) { +function* homeSaga(session: SevenWondersSession): * { yield [call(sendUsername, session), call(validateUsername, session)]; } diff --git a/frontend/src/sagas/lobby.js b/frontend/src/sagas/lobby.js index cc704086..93e0960f 100644 --- a/frontend/src/sagas/lobby.js +++ b/frontend/src/sagas/lobby.js @@ -1,24 +1,23 @@ -import { call, put, take, apply } from 'redux-saga/effects'; -import type { Channel } from 'redux-saga'; - -import { push } from 'react-router-redux'; - -import { normalize } from 'normalizr'; -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'; -import { createChannel } from './utils'; +// @flow +import { normalize } from 'normalizr' +import { push } from 'react-router-redux' +import type { Channel } from 'redux-saga' +import { eventChannel } from 'redux-saga' +import { apply, call, put, take } from 'redux-saga/effects' +import { SevenWondersSession } from '../api/sevenWondersApi' +import { actions as gameActions, types } from '../redux/games' +import { actions as playerActions } from '../redux/players' + +import { game as gameSchema } from '../schemas/games' function getCurrentGameId(): number { const path = window.location.pathname; return path.split('lobby/')[1]; } -function* watchLobbyUpdates(session: SevenWondersSession) { +function* watchLobbyUpdates(session: SevenWondersSession): * { const currentGameId: number = getCurrentGameId(); - const lobbyUpdatesChannel: Channel = yield createChannel(session, session.watchLobbyUpdated, currentGameId); + const lobbyUpdatesChannel: Channel = yield eventChannel(session.watchLobbyUpdated(currentGameId)); try { while (true) { const lobby = yield take(lobbyUpdatesChannel); @@ -31,9 +30,9 @@ function* watchLobbyUpdates(session: SevenWondersSession) { } } -function* watchGameStart(session: SevenWondersSession) { +function* watchGameStart(session: SevenWondersSession): * { const currentGameId = getCurrentGameId(); - const gameStartedChannel = yield createChannel(session, session.watchGameStarted, currentGameId); + const gameStartedChannel = yield eventChannel(session.watchGameStarted(currentGameId)); try { yield take(gameStartedChannel); yield put(gameActions.enterGame()); @@ -43,14 +42,14 @@ function* watchGameStart(session: SevenWondersSession) { } } -function* startGame(session: SevenWondersSession) { +function* startGame(session: SevenWondersSession): * { while (true) { yield take(types.REQUEST_START_GAME); yield apply(session, session.startGame, []); } } -function* lobbySaga(session: SevenWondersSession) { +function* lobbySaga(session: SevenWondersSession): * { yield [call(watchLobbyUpdates, session), call(watchGameStart, session), call(startGame, session)]; } diff --git a/frontend/src/sagas/utils.js b/frontend/src/sagas/utils.js deleted file mode 100644 index 28017c87..00000000 --- a/frontend/src/sagas/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -import { SevenWondersSession } from '../api/sevenWondersApi'; -import { eventChannel } from 'redux-saga'; - -type Emitter = (value: any) => void; - -export function createChannel(session: SevenWondersSession, methodWithCallback: () => void, ...args: Array) { - return eventChannel((emitter: Emitter) => { - return methodWithCallback.call(session, ...args, emitter); - }); -} -- cgit