Get to the point of working proof-of-work.

master
Icedream 2018-04-24 17:11:09 +02:00
parent 9ab0432a5b
commit adfcfb0ae0
Signed by: icedream
GPG Key ID: C1D30A06E6490C14
22 changed files with 435 additions and 78 deletions

View File

@ -103,6 +103,7 @@
"style-loader": "^0.21.0", "style-loader": "^0.21.0",
"stylus": "^0.54.5", "stylus": "^0.54.5",
"stylus-loader": "^3.0.2", "stylus-loader": "^3.0.2",
"typeface-changa": "^0.0.56",
"uglifyjs-webpack-plugin": "^1.2.5", "uglifyjs-webpack-plugin": "^1.2.5",
"url-loader": "^1.0.1", "url-loader": "^1.0.1",
"webfontloader": "^1.6.28", "webfontloader": "^1.6.28",

View File

@ -4,10 +4,16 @@ import { Provider } from 'react-redux';
import store from './redux'; import store from './redux';
import MessageBoxes from './containers/MessageBoxes'; import MessageBoxes from './containers/MessageBoxes';
import ServerTableWrapper from './components/ServerTableWrapper';
import style from './App.styl';
const App = () => ( const App = () => (
<Provider store={store}> <Provider store={store}>
<div className={style.app}>
<MessageBoxes /> <MessageBoxes />
<ServerTableWrapper />
</div>
</Provider> </Provider>
); );

View File

@ -0,0 +1,5 @@
@import '~typeface-changa'
.app
font-family: Changa, sans-serif
font-size: 16px

View File

@ -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 (
<span>
{'.'.repeat(this.state.dots)}
</span>
);
}
}

View File

@ -0,0 +1,32 @@
import React from 'react';
import style from './ServerTable.styl';
const ServerTable = ({ servers }) => (
<table className={style.serverTable}>
<thead>
<tr>
<th>Server Name</th>
<th>Variant</th>
<th>Map</th>
<th>Players</th>
<th>Server Address</th>
</tr>
</thead>
<tbody>
{
servers.map(server => (
<tr key={`${server.ip}:${server.port}`}>
<td>{server.name}</td>
<td>{server.variant}<br /><small>{server.variantType}</small></td>
<td>{server.map}</td>
<td>{server.numPlayers} / {server.maxPlayers}</td>
<td>{server.ip}:{server.port}</td>
</tr>
))
}
</tbody>
</table>
);
export default ServerTable;

View File

@ -0,0 +1,2 @@
.serverTable
width: 100%

View File

@ -0,0 +1,27 @@
import React from 'react';
import style from './ServerTableButtons.styl';
const ServerTableButtons = ({
disableRefreshButton,
hideCancelButton,
dispatchRefresh,
dispatchCancelRefresh,
}) => (
<div className={style.serverTableButtons}>
<button
disabled={disableRefreshButton}
onClick={dispatchRefresh}
>
Refresh
</button>
<button
disabled={hideCancelButton}
onClick={dispatchCancelRefresh}
>
Cancel refresh
</button>
</div>
);
export default ServerTableButtons;

View File

@ -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 }) => (
<div className={[
style.serverTableStatus,
state === serverlistLoader.states.LOADING
? style.serverTableStatusLoading
: style.serverTableStatusIdle,
].join(' ')}
>
<p>
{state === serverlistLoader.states.LOADING
? <span>Loading<AnimatedEllipsis /></span>
: 'Idle'}
</p>
<p>
{serverCount} servers
</p>
</div>
);
export default ServerTableStatus;

View File

@ -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 (
<div className={style.serverTableWrapper}>
<ServerTableStatus />
<ServerTableButtons />
<input type="text" value={this.state.queryText} onChange={this.handleQueryChange} />
<ServerTable filter={this.state.query} />
</div>
);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -37,9 +37,9 @@ window.addEventListener('load', () => {
<App /> <App />
), rootContainer); ), rootContainer);
setTimeout(() => { // setTimeout(() => {
store.dispatch(serverlist_loader.actions.updateGameServers()); // store.dispatch(serverlist_loader.actions.updateGameServers());
}, 500); // }, 15000);
// setTimeout(() => { // setTimeout(() => {
// store.dispatch(ui.actions.add({ // store.dispatch(ui.actions.add({

View File

@ -1,7 +1,11 @@
import { combineReducers } from 'redux'; // import { combineReducers } from 'redux';
const REDUX_PATH_SEPARATOR = '/'; const REDUX_PATH_SEPARATOR = '/';
function normalizeNamespace(name) {
return name.replace(/_(.)/, match => match[1].toUpperCase());
}
export default class ReduxGenerator { export default class ReduxGenerator {
constructor(...id) { constructor(...id) {
this._id = id; this._id = id;
@ -60,11 +64,22 @@ export default class ReduxGenerator {
.reduce((current, [ .reduce((current, [
name, name,
{ {
params, params = {},
reducer, reducer,
}, },
]) => { ]) => {
const id = this.actionType(name); 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 = { const target = {
...current, ...current,
actions: { actions: {
@ -74,24 +89,24 @@ export default class ReduxGenerator {
const err = Object.entries(payload) const err = Object.entries(payload)
.map(([key, value]) => { .map(([key, value]) => {
const validator = params[key]; const validator = params[key];
if (!validator) { if (validator === undefined || validator === null) {
console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`); console.warn(`${id}: extra argument ${JSON.stringify(key)}, ignoring`);
return undefined; return undefined;
} }
let result = validator(value); 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) { switch (typeof result) {
case 'boolean': case 'boolean': // validator returns either true on success or false on error
if (!result) { if (!result) {
result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`); result = new Error(`${JSON.stringify(key)}: invalid value: ${JSON.stringify(value)}`);
} else { } else {
result = null; result = null;
} }
break; break;
default: default: // error or string (or something else?)
if (result instanceof Error) { if (result instanceof Error) { // modify message to include key
result.message = `${JSON.stringify(key)}: ${result.message}`; result.message = `${JSON.stringify(key)}: ${result.message}`;
} else { } else { // use value as part of error reaosn
result = new Error(`${JSON.stringify(key)}: ${result}`); result = new Error(`${JSON.stringify(key)}: ${result}`);
} }
} }
@ -122,14 +137,14 @@ export default class ReduxGenerator {
[name]: (state, { type, payload }) => { [name]: (state, { type, payload }) => {
// Ignore non-matching actions // Ignore non-matching actions
if (type !== id) { if (type !== id) {
console.debug(`${type}: skipping action-mismatching reducer`, { state, payload }); // console.debug(`${type}: skipping action-mismatching reducer`, { state, payload });
return state; return state;
} }
// Call original reducer with our payload // 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); const result = reducer(state, payload);
console.debug(' =>', result); // console.debug(' =>', result);
return result; return result;
}, },
}; };
@ -142,9 +157,9 @@ export default class ReduxGenerator {
reducers: Object.entries(reducers) reducers: Object.entries(reducers)
.reduce((current, [name, reducer]) => ({ .reduce((current, [name, reducer]) => ({
[name]: (state, ...args) => { [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); const result = reducer(state, ...args);
console.debug(' =>', result); // console.debug(' =>', result);
return result; return result;
}, },
}), {}), }), {}),
@ -157,23 +172,24 @@ export default class ReduxGenerator {
[name]: (state, ...args) => { [name]: (state, ...args) => {
const namespaceState = this._id const namespaceState = this._id
.slice(1) // ignore root id .slice(1) // ignore root id
.map(normalizeNamespace)
.reduce( .reduce(
(currentState, namespaceName) => (currentState, namespaceName) =>
currentState[namespaceName], state); 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); const result = selector(namespaceState, ...args);
console.debug(' =>', result); // console.debug(' =>', result);
return result; return result;
}, },
}), selectors); }), selectors);
retval.reducer = (state, ...args) => { 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) { 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; const result = initialState;
console.debug(' =>', result); // console.debug(' =>', result);
return 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; return state;
}; };
@ -196,7 +212,7 @@ export default class ReduxGenerator {
// retval.initialState = initialState; // retval.initialState = initialState;
// } // }
console.debug('generated redux module:', retval); // console.debug('generated redux module:', retval);
return retval; return retval;
} }
} }

View File

@ -7,7 +7,7 @@ import {
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import { import {
all, all,
fork, // fork,
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import * as serverlist from './serverlist'; import * as serverlist from './serverlist';
@ -22,7 +22,7 @@ const reduxModules = Object.entries({
mastersync, mastersync,
}); });
console.debug('all redux modules', reduxModules); // console.debug('all redux modules', reduxModules);
const reducers = const reducers =
reduxModules reduxModules
@ -33,7 +33,7 @@ const reducers =
[name]: m.reducer, [name]: m.reducer,
}; };
}, {}); }, {});
console.debug('combined reducers:', reducers); // console.debug('combined reducers:', reducers);
const reducer = combineReducers(reducers); const reducer = combineReducers(reducers);
@ -50,10 +50,10 @@ const reducer = combineReducers(reducers);
// create the saga middleware // create the saga middleware
function* rootSaga() { function* rootSaga() {
const sagas = Object.values(reduxModules) const sagas = Object.values(reduxModules) // TODO - !?
.reduce((current, [name, m]) => { .reduce((current, [, m]) => {
if (!m.sagas) { if (!m.sagas) {
console.debug(`Module ${name} does not have any sagas`); // console.debug(`Module ${name} does not have any sagas`);
return current; return current;
} }
return [ return [
@ -61,9 +61,9 @@ function* rootSaga() {
...m.sagas, ...m.sagas,
]; ];
}, []); }, []);
console.debug('Will run these sagas from root:', sagas); // console.debug('Will run these sagas from root:', sagas);
yield all(sagas); yield all(sagas);
console.debug('Root saga done'); // console.debug('Root saga done');
} }
const sagaMiddleware = createSagaMiddleware(); const sagaMiddleware = createSagaMiddleware();

View File

@ -18,7 +18,6 @@ export const states = {
}; };
function* doDewritoJsonUpdateRequest() { function* doDewritoJsonUpdateRequest() {
console.debug('mastersync doDewritoJsonUpdateRequest saga called');
try { try {
const response = yield call(fetch, 'https://raw.githubusercontent.com/ElDewrito/ElDorito/master/dist/mods/dewrito.json'); const response = yield call(fetch, 'https://raw.githubusercontent.com/ElDewrito/ElDorito/master/dist/mods/dewrito.json');
if (response.status !== 200) { if (response.status !== 200) {
@ -33,16 +32,15 @@ function* doDewritoJsonUpdateRequest() {
err, err,
})); }));
} }
console.debug('mastersync doDewritoJsonUpdateRequest: done');
} }
function* storeMasters({ function* storeMasters(x) {
const {
payload: { payload: {
err, err,
dewritoJson, dewritoJson,
}, },
}) { } = x;
console.debug('mastersync storeMasters saga called');
if (err) { if (err) {
yield put(ui.actions.add({ yield put(ui.actions.add({
text: `Failed to fetch master server information: ${err.message}`, text: `Failed to fetch master server information: ${err.message}`,
@ -54,7 +52,6 @@ function* storeMasters({
masterServers, masterServers,
} = dewritoJson; } = dewritoJson;
yield put(myRedux.actions.setMasters({ masters: masterServers })); yield put(myRedux.actions.setMasters({ masters: masterServers }));
console.debug('mastersync storeMasters: done');
} }
export const initialState = { export const initialState = {

View File

@ -33,16 +33,16 @@ const myRedux = reduxGenerator.all({
variantType: check.maybe.string, variantType: check.maybe.string,
teamScores: check.maybe.array.of.number, teamScores: check.maybe.array.of.number,
status: check.string, status: check.string,
numPlayers: check.positiveInteger, numPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)),
mods: check.array, // TODO - fine tune this mods: check.array, // TODO - fine tune this
maxPlayers: check.positiveInteger, maxPlayers: v => check.integer(v) && (check.zero(v) || check.positive(v)),
passworded: check.boolean, passworded: check.boolean,
isDedicated: check.boolean, isDedicated: check.boolean,
gameVersion: check.string, gameVersion: check.string,
eldewritoVersion: check.maybe.string, eldewritoVersion: check.maybe.string,
xnkid: check.maybe.string, xnkid: check.maybe.string,
xnaddr: check.maybe.string, xnaddr: check.maybe.string,
players: check.maybe.array.of.like({ players: v => check.maybe.array.of.like(v, {
name: '', name: '',
serviceTag: '', serviceTag: '',
team: 0, team: 0,
@ -59,16 +59,19 @@ const myRedux = reduxGenerator.all({
bestStreak: 0, bestStreak: 0,
}), }),
}, },
reducer(current, payload) { reducer(current, payload) {
// do we have this server on the list already? // do we have this server on the list already?
if (current.servers.find(server => server.ip === payload.ip)) { if (current.servers.find(server =>
server.ip === payload.ip
&& server.port === payload.port)) {
// skip this server // skip this server
return current; return current;
} }
// add server // add server
return { return {
current, ...current,
servers: [ servers: [
...current.servers, ...current.servers,
payload, 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: { reset: {
reducer: () => initialState, reducer: () => initialState,
}, },
@ -86,19 +110,22 @@ const myRedux = reduxGenerator.all({
selectors: { selectors: {
/* /*
Example: Example:
filter(state, { getServers(state, {
name: { name: {
$in: ["substring"], $in: ["substring"],
} }
gameType: "slayer", gameType: "slayer",
}) })
*/ */
filterServers({ servers }, filter, { order } = {}) { getServers({ servers }, filter, { order } = {}) {
let filtered = obop.where(servers, filter); let finalServers = servers;
if (order) { if (filter) {
filtered = obop.order(filtered, order); finalServers = obop.where(finalServers, filter);
} }
return filtered; if (order) {
finalServers = obop.order(finalServers, order);
}
return finalServers;
}, },
}, },
}); });

View File

@ -4,8 +4,10 @@ import {
call, call,
select, select,
all, all,
race,
put, put,
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import check from '../check'; import check from '../check';
@ -23,6 +25,7 @@ export const states = {
export const initialState = { export const initialState = {
state: states.IDLE, state: states.IDLE,
bufferedServers: [],
}; };
function* inspectGameServer(addr) { function* inspectGameServer(addr) {
@ -41,21 +44,23 @@ function* inspectGameServer(addr) {
throw new Error(`Unexpected HTTP error code ${response.status}: ${response.statusText}`); throw new Error(`Unexpected HTTP error code ${response.status}: ${response.statusText}`);
} }
const serverInfo = yield call(response.json.bind(response)); const serverInfo = yield call(response.json.bind(response));
check.assert.string(serverInfo.name); // check.assert.string(serverInfo.name);
check.assert(check.port(serverInfo.port)); // check.assert(check.port(serverInfo.port));
check.assert.maybe.string(serverInfo.hostPlayer); // check.assert.maybe.string(serverInfo.hostPlayer);
check.assert.maybe.string(serverInfo.map); // check.assert.maybe.string(serverInfo.map);
check.assert.maybe.string(serverInfo.mapFile); // check.assert.maybe.string(serverInfo.mapFile);
check.assert.maybe.string(serverInfo.variant); // check.assert.maybe.string(serverInfo.variant);
check.assert.maybe.string(serverInfo.variantType); // check.assert.maybe.string(serverInfo.variantType);
check.assert.integer(serverInfo.maxPlayers); // check.assert.integer(serverInfo.maxPlayers);
check.assert.integer(serverInfo.numPlayers); // check.assert.integer(serverInfo.numPlayers);
check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers); // check.assert.inRange(serverInfo.numPlayers, 0, serverInfo.maxPlayers);
check.assert.array(serverInfo.players); // check.assert.array(serverInfo.players);
yield put(serverlist.actions.add({ yield put(myRedux.actions.updateGameServersBuffer({
server: {
ip, ip,
port, port,
...serverInfo, ...serverInfo,
},
})); }));
} catch (e) { } catch (e) {
console.error(`Request to game server ${addr} failed:`, e); console.error(`Request to game server ${addr} failed:`, e);
@ -83,7 +88,9 @@ function* updateGameServersFromMaster(masterListUrl) {
if (code !== 0) { if (code !== 0) {
throw new Error(`Server returned error code ${code}: ${msg}`); 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; break;
default: default:
throw new Error(`Unsupported list version: ${listVersion}`); throw new Error(`Unsupported list version: ${listVersion}`);
@ -94,7 +101,7 @@ function* updateGameServersFromMaster(masterListUrl) {
} }
function* updateGameServers() { function* updateGameServers() {
yield put(mastersync.actions.updateDewritoJsonStarted()); yield put(myRedux.actions.updateGameServersStarted());
// First let's ensure we have master servers at all // First let's ensure we have master servers at all
if (!(yield select(mastersync.selectors.hasMasters))) { if (!(yield select(mastersync.selectors.hasMasters))) {
@ -102,7 +109,7 @@ function* updateGameServers() {
yield put(mastersync.actions.updateDewritoJson()); yield put(mastersync.actions.updateDewritoJson());
const { err } = yield take(mastersync.actionTypes.updateDewritoJsonFinished); const { err } = yield take(mastersync.actionTypes.updateDewritoJsonFinished);
if (err) { if (err) {
yield put(mastersync.actions.updateDewritoJsonFinished({ err })); yield put(myRedux.actions.updateGameServersFinished({ err }));
return; return;
} }
} }
@ -111,15 +118,38 @@ function* updateGameServers() {
yield put(serverlist.actions.reset()); yield put(serverlist.actions.reset());
const masterListUrls = (yield select(mastersync.selectors.getMasters)) const masterListUrls = (yield select(mastersync.selectors.getMasters))
.map(server => server.list); .map(server => server.list);
yield all(masterListUrls yield race([
.map(url => call(updateGameServersFromMaster, url))); all(masterListUrls.map(url => call(updateGameServersFromMaster, url))),
yield put(mastersync.actions.updateDewritoJsonFinished({})); 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({ myRedux = root.sub('serverlist_loader').all({
initialState, initialState,
actions: { actions: {
cancelUpdateGameServers: {},
updateGameServers: {}, updateGameServers: {},
updateGameServersStarted: { 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: { updateGameServersFinished: {
reducer: state => ({ reducer: state => ({
...state, ...state,
@ -140,11 +186,13 @@ myRedux = root.sub('serverlist_loader').all({
// selectors are all function(state, ...parameters) // selectors are all function(state, ...parameters)
selectors: { selectors: {
getState: ({ state }) => state, getState: ({ state }) => state,
getBufferedServers: ({ bufferedServers }) => bufferedServers,
}, },
}); });
export const sagas = [ export const sagas = [
takeEvery(myRedux.actionTypes.updateGameServers, updateGameServers), takeEvery(myRedux.actionTypes.updateGameServers, updateGameServers),
takeEvery(myRedux.actionTypes.updateGameServersBuffer, pushBufferToGameServersList),
]; ];
export const { export const {

View File

@ -64,8 +64,8 @@ export default (options) => {
.replace(/\[chunkhash(.*?)\]/g, '[hash$1]'); .replace(/\[chunkhash(.*?)\]/g, '[hash$1]');
const cssOutputFileName = baseOutputFilename const cssOutputFileName = baseOutputFilename
.replace(/\[ext(.*?)\]/g, 'css') .replace(/\[ext(.*?)\]/g, 'css');
.replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]'); // .replace(/\[chunkhash(.*?)\]/g, '[contenthash$1]');
// const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`; // const cssOutputRebasePath = `${slash(path.relative(path.dirname(cssOutputFileName), ''))}/`;
const cssOutputRebasePath = '/'; const cssOutputRebasePath = '/';

View File

@ -8383,6 +8383,10 @@ typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 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: ua-parser-js@^0.7.9:
version "0.7.17" version "0.7.17"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"