diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 40bea7f..cf088af 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -103,6 +103,7 @@ "style-loader": "^0.21.0", "stylus": "^0.54.5", "stylus-loader": "^3.0.2", + "typeface-changa": "^0.0.56", "uglifyjs-webpack-plugin": "^1.2.5", "url-loader": "^1.0.1", "webfontloader": "^1.6.28", diff --git a/packages/frontend/src/website/App.jsx b/packages/frontend/src/website/App.jsx index 84fa793..bf5d30d 100644 --- a/packages/frontend/src/website/App.jsx +++ b/packages/frontend/src/website/App.jsx @@ -4,10 +4,16 @@ import { Provider } from 'react-redux'; import store from './redux'; import MessageBoxes from './containers/MessageBoxes'; +import ServerTableWrapper from './components/ServerTableWrapper'; + +import style from './App.styl'; const App = () => ( - +
+ + +
); diff --git a/packages/frontend/src/website/App.styl b/packages/frontend/src/website/App.styl new file mode 100644 index 0000000..9288069 --- /dev/null +++ b/packages/frontend/src/website/App.styl @@ -0,0 +1,5 @@ +@import '~typeface-changa' + +.app + font-family: Changa, sans-serif + font-size: 16px diff --git a/packages/frontend/src/website/components/AnimatedEllipsis.jsx b/packages/frontend/src/website/components/AnimatedEllipsis.jsx new file mode 100644 index 0000000..5f5b731 --- /dev/null +++ b/packages/frontend/src/website/components/AnimatedEllipsis.jsx @@ -0,0 +1,54 @@ +import React from 'react'; + +export default class AnimatedEllipsis extends React.Component { + constructor() { + super(); + + this.state = { + dots: 0, + }; + + this.timer = null; + + this.componentDidMount = this.componentDidMount.bind(this); + this.componentWillUnmount = this.componentWillUnmount.bind(this); + this.startDotTimer = this.startDotTimer.bind(this); + this.stopDotTimer = this.stopDotTimer.bind(this); + this.incrementDots = this.incrementDots.bind(this); + } + + componentDidMount() { + this.startDotTimer(); + } + + componentWillUnmount() { + this.stopDotTimer(); + } + + startDotTimer() { + if (this.timer === null) { + this.timer = setInterval(this.incrementDots, 250); + } + } + + stopDotTimer() { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + } + + incrementDots() { + this.setState(state => ({ + dots: (state.dots + 1) % 4, + })); + } + + render() { + return ( + + {'.'.repeat(this.state.dots)} + + ); + } +} diff --git a/packages/frontend/src/website/components/ServerTable.jsx b/packages/frontend/src/website/components/ServerTable.jsx new file mode 100644 index 0000000..1fe8bdc --- /dev/null +++ b/packages/frontend/src/website/components/ServerTable.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import style from './ServerTable.styl'; + +const ServerTable = ({ servers }) => ( + + + + + + + + + + + + { + servers.map(server => ( + + + + + + + + )) + } + +
Server NameVariantMapPlayersServer Address
{server.name}{server.variant}
{server.variantType}
{server.map}{server.numPlayers} / {server.maxPlayers}{server.ip}:{server.port}
+); + +export default ServerTable; diff --git a/packages/frontend/src/website/components/ServerTable.styl b/packages/frontend/src/website/components/ServerTable.styl new file mode 100644 index 0000000..5d64d5f --- /dev/null +++ b/packages/frontend/src/website/components/ServerTable.styl @@ -0,0 +1,2 @@ +.serverTable + width: 100% diff --git a/packages/frontend/src/website/components/ServerTableButtons.jsx b/packages/frontend/src/website/components/ServerTableButtons.jsx new file mode 100644 index 0000000..1743c9c --- /dev/null +++ b/packages/frontend/src/website/components/ServerTableButtons.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import style from './ServerTableButtons.styl'; + +const ServerTableButtons = ({ + disableRefreshButton, + hideCancelButton, + dispatchRefresh, + dispatchCancelRefresh, +}) => ( +
+ + +
+); + +export default ServerTableButtons; diff --git a/packages/frontend/src/website/components/ServerTableButtons.styl b/packages/frontend/src/website/components/ServerTableButtons.styl new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/frontend/src/website/components/ServerTableButtons.styl @@ -0,0 +1 @@ + diff --git a/packages/frontend/src/website/components/ServerTableStatus.jsx b/packages/frontend/src/website/components/ServerTableStatus.jsx new file mode 100644 index 0000000..3d852ad --- /dev/null +++ b/packages/frontend/src/website/components/ServerTableStatus.jsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import * as serverlistLoader from '../redux/serverlist_loader'; + +import AnimatedEllipsis from './AnimatedEllipsis'; + +import style from './ServerTable.styl'; + + +const ServerTableStatus = ({ serverCount, state }) => ( +
+

+ {state === serverlistLoader.states.LOADING + ? Loading + : 'Idle'} +

+

+ {serverCount} servers +

+
+); + +export default ServerTableStatus; diff --git a/packages/frontend/src/website/components/ServerTableWrapper.jsx b/packages/frontend/src/website/components/ServerTableWrapper.jsx new file mode 100644 index 0000000..e93ecda --- /dev/null +++ b/packages/frontend/src/website/components/ServerTableWrapper.jsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import style from './ServerTableWrapper.styl'; + +import ServerTable from '../containers/ServerTable'; +import ServerTableStatus from '../containers/ServerTableStatus'; +import ServerTableButtons from '../containers/ServerTableButtons'; + +export default class ServerTableWrapper extends React.Component { + constructor() { + super(); + this.state = { + query: undefined, + queryText: '', + }; + this.handleQueryChange = this.handleQueryChange.bind(this); + } + + handleQueryChange(e) { + const queryText = e.target.value; + this.setState({ + queryText, + }); + if (queryText && queryText.length > 0) { + this.setState({ + query: item => item.name.toLowerCase().includes(queryText.toLowerCase()), + }); + } else { + this.setState({ + query: undefined, + }); + } + } + + render() { + return ( +
+ + + + +
+ ); + } +} diff --git a/packages/frontend/src/website/components/ServerTableWrapper.styl b/packages/frontend/src/website/components/ServerTableWrapper.styl new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/frontend/src/website/components/ServerTableWrapper.styl @@ -0,0 +1 @@ + diff --git a/packages/frontend/src/website/containers/ServerTable.js b/packages/frontend/src/website/containers/ServerTable.js new file mode 100644 index 0000000..03c8c6d --- /dev/null +++ b/packages/frontend/src/website/containers/ServerTable.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; + +import { selectors } from '../redux/serverlist'; + +import Component from '../components/ServerTable'; + +const mapStateToProps = (state, { filter }) => { + // TODO - filter + const servers = selectors.getServers(state, filter); + return ({ + servers: servers.slice(0, 25), + }); +}; + +const ConnectedMessageBoxes = connect( + mapStateToProps, +)(Component); + +export default ConnectedMessageBoxes; diff --git a/packages/frontend/src/website/containers/ServerTableButtons.js b/packages/frontend/src/website/containers/ServerTableButtons.js new file mode 100644 index 0000000..07f1635 --- /dev/null +++ b/packages/frontend/src/website/containers/ServerTableButtons.js @@ -0,0 +1,26 @@ +import { connect } from 'react-redux'; + +import * as serverlistLoader from '../redux/serverlist_loader'; + +import Component from '../components/ServerTableButtons'; + +const mapStateToProps = state => ({ + hideCancelButton: serverlistLoader.selectors.getState(state) === serverlistLoader.states.IDLE, + disableRefreshButton: serverlistLoader.selectors.getState(state) !== serverlistLoader.states.IDLE, +}); + +const mapDispatchToProps = dispatch => ({ + dispatchRefresh: () => { + dispatch(serverlistLoader.actions.updateGameServers()); + }, + dispatchCancelRefresh: () => { + dispatch(serverlistLoader.actions.cancelUpdateGameServers()); + }, +}); + +const ConnectedMessageBoxes = connect( + mapStateToProps, + mapDispatchToProps, +)(Component); + +export default ConnectedMessageBoxes; diff --git a/packages/frontend/src/website/containers/ServerTableStatus.js b/packages/frontend/src/website/containers/ServerTableStatus.js new file mode 100644 index 0000000..a4a63fc --- /dev/null +++ b/packages/frontend/src/website/containers/ServerTableStatus.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; + +import * as serverlist from '../redux/serverlist'; +import * as serverlistLoader from '../redux/serverlist_loader'; + +import Component from '../components/ServerTableStatus'; + +const mapStateToProps = state => ({ + state: serverlistLoader.selectors.getState(state), + serverCount: serverlist.selectors.getServers(state).length, +}); + +const ConnectedMessageBoxes = connect( + mapStateToProps, +)(Component); + +export default ConnectedMessageBoxes; diff --git a/packages/frontend/src/website/index.jsx b/packages/frontend/src/website/index.jsx index a99ca83..727a8ed 100644 --- a/packages/frontend/src/website/index.jsx +++ b/packages/frontend/src/website/index.jsx @@ -37,9 +37,9 @@ window.addEventListener('load', () => { ), rootContainer); - setTimeout(() => { - store.dispatch(serverlist_loader.actions.updateGameServers()); - }, 500); + // setTimeout(() => { + // store.dispatch(serverlist_loader.actions.updateGameServers()); + // }, 15000); // setTimeout(() => { // store.dispatch(ui.actions.add({ diff --git a/packages/frontend/src/website/redux/base.js b/packages/frontend/src/website/redux/base.js index 17fdb0b..1572507 100644 --- a/packages/frontend/src/website/redux/base.js +++ b/packages/frontend/src/website/redux/base.js @@ -1,7 +1,11 @@ -import { combineReducers } from 'redux'; +// import { combineReducers } from 'redux'; const REDUX_PATH_SEPARATOR = '/'; +function normalizeNamespace(name) { + return name.replace(/_(.)/, match => match[1].toUpperCase()); +} + export default class ReduxGenerator { constructor(...id) { this._id = id; @@ -60,11 +64,22 @@ export default class ReduxGenerator { .reduce((current, [ name, { - params, + params = {}, reducer, }, ]) => { const id = this.actionType(name); + + // Validate params, they should all have a validator function as a value + if (params) { + const keysWithNonFunctionValidator = Object.keys(params) + .filter(paramName => typeof (params[paramName]) !== 'function') + .map(v => JSON.stringify(v)); + if (keysWithNonFunctionValidator.length > 0) { + throw new Error(`Following params of ${JSON.stringify(id)} have a non-function value as validator: ${keysWithNonFunctionValidator.join(', ')}`); + } + } + const target = { ...current, actions: { @@ -74,24 +89,24 @@ export default class ReduxGenerator { const err = Object.entries(payload) .map(([key, value]) => { const validator = params[key]; - if (!validator) { + if (validator === undefined || validator === null) { console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`); return undefined; } let result = validator(value); - console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result); + // console.debug(`${id}: verifying ${JSON.stringify(key)} result:`, result); switch (typeof result) { - case 'boolean': + case 'boolean': // validator returns either true on success or false on error if (!result) { result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`); } else { result = null; } break; - default: - if (result instanceof Error) { + default: // error or string (or something else?) + if (result instanceof Error) { // modify message to include key result.message = `${JSON.stringify(key)}: ${result.message}`; - } else { + } else { // use value as part of error reaosn result = new Error(`${JSON.stringify(key)}: ${result}`); } } @@ -122,14 +137,14 @@ export default class ReduxGenerator { [name]: (state, { type, payload }) => { // Ignore non-matching actions if (type !== id) { - console.debug(`${type}: skipping action-mismatching reducer`, { state, payload }); + // console.debug(`${type}: skipping action-mismatching reducer`, { state, payload }); return state; } // Call original reducer with our payload - console.debug(`${type}: action-matched reducer called`, { state, payload }); + // console.debug(`${type}: action-matched reducer called`, { state, payload }); const result = reducer(state, payload); - console.debug(' =>', result); + // console.debug(' =>', result); return result; }, }; @@ -142,9 +157,9 @@ export default class ReduxGenerator { reducers: Object.entries(reducers) .reduce((current, [name, reducer]) => ({ [name]: (state, ...args) => { - console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args }); + // console.debug(`${this._id.join('/', `reducer:${name}`)}: called`, { state, args }); const result = reducer(state, ...args); - console.debug(' =>', result); + // console.debug(' =>', result); return result; }, }), {}), @@ -157,23 +172,24 @@ export default class ReduxGenerator { [name]: (state, ...args) => { const namespaceState = this._id .slice(1) // ignore root id + .map(normalizeNamespace) .reduce( (currentState, namespaceName) => currentState[namespaceName], state); - console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args }); + // console.debug(`${[...this._id, `selector:${name}`].join('/')}: called`, { state: namespaceState, args }); const result = selector(namespaceState, ...args); - console.debug(' =>', result); + // console.debug(' =>', result); return result; }, }), selectors); retval.reducer = (state, ...args) => { - console.debug(`${this._id.join('/')}: combined reducer called`, { state, args }); + // console.debug(`${this._id.join('/')}: combined reducer called`, { state, args }); if (state === undefined) { - console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args }); + // console.debug(`${this._id.join('/')}: undefined state passed to reducer`, { state, args }); const result = initialState; - console.debug(' =>', result); + // console.debug(' =>', result); return result; } @@ -188,7 +204,7 @@ export default class ReduxGenerator { ); } - console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`); + // console.debug(`${this._id.join('/')}: no reducers for this namespace, skipping`); return state; }; @@ -196,7 +212,7 @@ export default class ReduxGenerator { // retval.initialState = initialState; // } - console.debug('generated redux module:', retval); + // console.debug('generated redux module:', retval); return retval; } } diff --git a/packages/frontend/src/website/redux/index.js b/packages/frontend/src/website/redux/index.js index bc167e9..d268d5a 100644 --- a/packages/frontend/src/website/redux/index.js +++ b/packages/frontend/src/website/redux/index.js @@ -7,7 +7,7 @@ import { import createSagaMiddleware from 'redux-saga'; import { all, - fork, + // fork, } from 'redux-saga/effects'; import * as serverlist from './serverlist'; @@ -22,7 +22,7 @@ const reduxModules = Object.entries({ mastersync, }); -console.debug('all redux modules', reduxModules); +// console.debug('all redux modules', reduxModules); const reducers = reduxModules @@ -33,7 +33,7 @@ const reducers = [name]: m.reducer, }; }, {}); -console.debug('combined reducers:', reducers); +// console.debug('combined reducers:', reducers); const reducer = combineReducers(reducers); @@ -50,10 +50,10 @@ const reducer = combineReducers(reducers); // create the saga middleware function* rootSaga() { - const sagas = Object.values(reduxModules) - .reduce((current, [name, m]) => { + const sagas = Object.values(reduxModules) // TODO - !? + .reduce((current, [, m]) => { if (!m.sagas) { - console.debug(`Module ${name} does not have any sagas`); + // console.debug(`Module ${name} does not have any sagas`); return current; } return [ @@ -61,9 +61,9 @@ function* rootSaga() { ...m.sagas, ]; }, []); - console.debug('Will run these sagas from root:', sagas); + // console.debug('Will run these sagas from root:', sagas); yield all(sagas); - console.debug('Root saga done'); + // console.debug('Root saga done'); } const sagaMiddleware = createSagaMiddleware(); diff --git a/packages/frontend/src/website/redux/mastersync/index.js b/packages/frontend/src/website/redux/mastersync/index.js index 2a13282..39c3d2a 100644 --- a/packages/frontend/src/website/redux/mastersync/index.js +++ b/packages/frontend/src/website/redux/mastersync/index.js @@ -18,7 +18,6 @@ export const states = { }; function* doDewritoJsonUpdateRequest() { - console.debug('mastersync doDewritoJsonUpdateRequest saga called'); try { const response = yield call(fetch, 'https://raw.githubusercontent.com/ElDewrito/ElDorito/master/dist/mods/dewrito.json'); if (response.status !== 200) { @@ -33,16 +32,15 @@ function* doDewritoJsonUpdateRequest() { err, })); } - console.debug('mastersync doDewritoJsonUpdateRequest: done'); } -function* storeMasters({ - payload: { - err, - dewritoJson, - }, -}) { - console.debug('mastersync storeMasters saga called'); +function* storeMasters(x) { + const { + payload: { + err, + dewritoJson, + }, + } = x; if (err) { yield put(ui.actions.add({ text: `Failed to fetch master server information: ${err.message}`, @@ -54,7 +52,6 @@ function* storeMasters({ masterServers, } = dewritoJson; yield put(myRedux.actions.setMasters({ masters: masterServers })); - console.debug('mastersync storeMasters: done'); } export const initialState = { diff --git a/packages/frontend/src/website/redux/serverlist/index.js b/packages/frontend/src/website/redux/serverlist/index.js index 01263ce..6f1d464 100644 --- a/packages/frontend/src/website/redux/serverlist/index.js +++ b/packages/frontend/src/website/redux/serverlist/index.js @@ -33,16 +33,16 @@ const myRedux = reduxGenerator.all({ variantType: check.maybe.string, teamScores: check.maybe.array.of.number, status: check.string, - numPlayers: check.positiveInteger, + numPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)), mods: check.array, // TODO - fine tune this - maxPlayers: check.positiveInteger, + maxPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)), passworded: check.boolean, isDedicated: check.boolean, gameVersion: check.string, eldewritoVersion: check.maybe.string, xnkid: check.maybe.string, xnaddr: check.maybe.string, - players: check.maybe.array.of.like({ + players: v => check.maybe.array.of.like(v, { name: '', serviceTag: '', team: 0, @@ -59,16 +59,19 @@ const myRedux = reduxGenerator.all({ bestStreak: 0, }), }, + reducer(current, payload) { - // do we have this server on the list already? - if (current.servers.find(server => server.ip === payload.ip)) { - // skip this server + // do we have this server on the list already? + if (current.servers.find(server => + server.ip === payload.ip + && server.port === payload.port)) { + // skip this server return current; } // add server return { - current, + ...current, servers: [ ...current.servers, payload, @@ -77,6 +80,27 @@ const myRedux = reduxGenerator.all({ }, }, + addMultiple: { + params: { + servers: v => !!v, // TODO + }, + reducer(current, { servers }) { + return { ...current, + servers: servers.reduce((currentServers, server) => { + // is this an actual new server? + if (!currentServers.find(registeredServer => + server.ip === registeredServer.ip + && server.port === registeredServer.port)) { + return [...currentServers, server]; + } + + // server is not new, return old list + return currentServers; + }, current.servers), + }; + }, + }, + reset: { reducer: () => initialState, }, @@ -86,19 +110,22 @@ const myRedux = reduxGenerator.all({ selectors: { /* Example: - filter(state, { + getServers(state, { name: { $in: ["substring"], } gameType: "slayer", }) */ - filterServers({ servers }, filter, { order } = {}) { - let filtered = obop.where(servers, filter); - if (order) { - filtered = obop.order(filtered, order); + getServers({ servers }, filter, { order } = {}) { + let finalServers = servers; + if (filter) { + finalServers = obop.where(finalServers, filter); } - return filtered; + if (order) { + finalServers = obop.order(finalServers, order); + } + return finalServers; }, }, }); diff --git a/packages/frontend/src/website/redux/serverlist_loader/index.js b/packages/frontend/src/website/redux/serverlist_loader/index.js index 285c2a8..dbab72c 100644 --- a/packages/frontend/src/website/redux/serverlist_loader/index.js +++ b/packages/frontend/src/website/redux/serverlist_loader/index.js @@ -4,8 +4,10 @@ import { call, select, all, + race, put, } from 'redux-saga/effects'; +import { delay } from 'redux-saga'; import fetch from 'node-fetch'; import check from '../check'; @@ -23,6 +25,7 @@ export const states = { export const initialState = { state: states.IDLE, + bufferedServers: [], }; function* inspectGameServer(addr) { @@ -41,21 +44,23 @@ function* inspectGameServer(addr) { throw new Error(`Unexpected HTTP error code ${response.status}: ${response.statusText}`); } const serverInfo = yield call(response.json.bind(response)); - check.assert.string(serverInfo.name); - check.assert(check.port(serverInfo.port)); - check.assert.maybe.string(serverInfo.hostPlayer); - check.assert.maybe.string(serverInfo.map); - check.assert.maybe.string(serverInfo.mapFile); - check.assert.maybe.string(serverInfo.variant); - check.assert.maybe.string(serverInfo.variantType); - check.assert.integer(serverInfo.maxPlayers); - check.assert.integer(serverInfo.numPlayers); - check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers); - check.assert.array(serverInfo.players); - yield put(serverlist.actions.add({ - ip, - port, - ...serverInfo, + // check.assert.string(serverInfo.name); + // check.assert(check.port(serverInfo.port)); + // check.assert.maybe.string(serverInfo.hostPlayer); + // check.assert.maybe.string(serverInfo.map); + // check.assert.maybe.string(serverInfo.mapFile); + // check.assert.maybe.string(serverInfo.variant); + // check.assert.maybe.string(serverInfo.variantType); + // check.assert.integer(serverInfo.maxPlayers); + // check.assert.integer(serverInfo.numPlayers); + // check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers); + // check.assert.array(serverInfo.players); + yield put(myRedux.actions.updateGameServersBuffer({ + server: { + ip, + port, + ...serverInfo, + }, })); } catch (e) { console.error(`Request to game server ${addr} failed:`, e); @@ -83,7 +88,9 @@ function* updateGameServersFromMaster(masterListUrl) { if (code !== 0) { throw new Error(`Server returned error code ${code}: ${msg}`); } - yield all(servers.map(addr => call(inspectGameServer, addr))); + yield all(servers + // .slice(0, 1) // DEBUG + .map(addr => call(inspectGameServer, addr))); break; default: throw new Error(`Unsupported list version: ${listVersion}`); @@ -94,7 +101,7 @@ function* updateGameServersFromMaster(masterListUrl) { } function* updateGameServers() { - yield put(mastersync.actions.updateDewritoJsonStarted()); + yield put(myRedux.actions.updateGameServersStarted()); // First let's ensure we have master servers at all if (!(yield select(mastersync.selectors.hasMasters))) { @@ -102,7 +109,7 @@ function* updateGameServers() { yield put(mastersync.actions.updateDewritoJson()); const { err } = yield take(mastersync.actionTypes.updateDewritoJsonFinished); if (err) { - yield put(mastersync.actions.updateDewritoJsonFinished({ err })); + yield put(myRedux.actions.updateGameServersFinished({ err })); return; } } @@ -111,15 +118,38 @@ function* updateGameServers() { yield put(serverlist.actions.reset()); const masterListUrls = (yield select(mastersync.selectors.getMasters)) .map(server => server.list); - yield all(masterListUrls - .map(url => call(updateGameServersFromMaster, url))); - yield put(mastersync.actions.updateDewritoJsonFinished({})); + yield race([ + all(masterListUrls.map(url => call(updateGameServersFromMaster, url))), + take(myRedux.actionTypes.cancelUpdateGameServers), + ]); + yield put(myRedux.actions.updateGameServersFinished()); +} + +function* pushBufferToGameServersList() { + // wait 750 milliseconds or until serverlist checks have all finished + const [, cancel] = yield race([ + delay(250), + take(myRedux.actionTypes.updateGameServersBuffer), + take(myRedux.actionTypes.updateGameServersFinished), + ]); + if (cancel) return; + + const servers = yield select(myRedux.selectors.getBufferedServers); + yield put(myRedux.actions.updateGameServersBuffer({ + clear: true, + })); + // console.log(`Would have added ${servers.length} new servers`); + yield put(serverlist.actions.addMultiple({ + servers, + })); } myRedux = root.sub('serverlist_loader').all({ initialState, actions: { + cancelUpdateGameServers: {}, + updateGameServers: {}, updateGameServersStarted: { @@ -129,6 +159,22 @@ myRedux = root.sub('serverlist_loader').all({ }), }, + updateGameServersBuffer: { + params: { + server: () => true, // TODO + clear: check.maybe.boolean, + }, + reducer: (state, { server, clear }) => ({ + ...state, + bufferedServers: clear + ? [] + : [ + ...state.bufferedServers, + server, + ], + }), + }, + updateGameServersFinished: { reducer: state => ({ ...state, @@ -140,11 +186,13 @@ myRedux = root.sub('serverlist_loader').all({ // selectors are all function(state, ...parameters) selectors: { getState: ({ state }) => state, + getBufferedServers: ({ bufferedServers }) => bufferedServers, }, }); export const sagas = [ takeEvery(myRedux.actionTypes.updateGameServers, updateGameServers), + takeEvery(myRedux.actionTypes.updateGameServersBuffer, pushBufferToGameServersList), ]; export const { diff --git a/packages/frontend/webpack.config.babel.js b/packages/frontend/webpack.config.babel.js index 9e4fee0..4c2ee3f 100644 --- a/packages/frontend/webpack.config.babel.js +++ b/packages/frontend/webpack.config.babel.js @@ -64,8 +64,8 @@ export default (options) => { .replace(/\[chunkhash(.*?)\]/g, '[hash$1]'); const cssOutputFileName = baseOutputFilename - .replace(/\[ext(.*?)\]/g, 'css') - .replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]'); + .replace(/\[ext(.*?)\]/g, 'css'); + // .replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]'); // const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`; const cssOutputRebasePath = '/'; diff --git a/yarn.lock b/yarn.lock index 6bfeae9..a3863b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8383,6 +8383,10 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typeface-changa@^0.0.56: + version "0.0.56" + resolved "https://registry.yarnpkg.com/typeface-changa/-/typeface-changa-0.0.56.tgz#df3ea2e8c9835987917bb0c5aad147be184f5d09" + ua-parser-js@^0.7.9: version "0.7.17" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"