Skip to content

Commit

Permalink
Compactify transferdata (deltalog, _initial) (#252)
Browse files Browse the repository at this point in the history
* drop _initial on action results

* transfer only deltalogs on actions sent to server

* change reducer to only return a deltalog

* push coverage to 100% by dropping unused statements

* separate log object and socket events that modify it

Split the socket events into:

1. SYNC

This is called at the beginning of the connection. The server
sends back the entire state and the entire log.

2. UPDATE

This is called after each move (it is broadacast to all clients).
The server sends back a deltalog and the entire state.

The log object is maintained in client.js via Redux middleware.

On the server, it is still kept inside the state object, although
a future change will split it into a separate area of storage
as an optimization.

* remove unnecessary Object.assign from server code

* refactor some tests

* minor fix
  • Loading branch information
Stefan-Hanke authored and nicolodavis committed Aug 13, 2018
1 parent 314f8f0 commit 500ecc3
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 256 deletions.
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ get done before we are ready for a v1 release.
### Lobby

* [x] basic `create` and `join` API
* [ ] simple web-based lobby ([issue](https://github.com/google/boardgame.io/issues/197)) **[help needed]**
* [ ] simple web-based lobby ([issue](https://github.com/google/boardgame.io/issues/197)) **[help needed]**

### Storage

Expand Down
53 changes: 51 additions & 2 deletions src/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* https://opensource.org/licenses/MIT.
*/

import { createStore } from 'redux';
import { createStore, compose, applyMiddleware } from 'redux';
import * as Actions from '../core/action-types';
import * as ActionCreators from '../core/action-creators';
import { Multiplayer } from './multiplayer/multiplayer';
import { CreateGameReducer } from '../core/reducer';
Expand Down Expand Up @@ -132,6 +133,54 @@ class _ClientImpl {
};

this.store = null;
this.log = [];

/**
* Middleware that manages the log object.
* Reducers generate deltalogs, which are log events
* that are the result of application of a single action.
* The server may also send back a deltalog or the entire
* log depending on the type of socket request.
* The middleware below takes care of all these cases while
* managing the log object.
*/
const LogMiddleware = store => next => action => {
const result = next(action);
const state = store.getState();

switch (action.type) {
case Actions.MAKE_MOVE:
case Actions.GAME_EVENT: {
const deltalog = state.deltalog;
this.log = [...this.log, ...deltalog];
break;
}

case Actions.RESET: {
this.log = [];
break;
}

case Actions.UPDATE: {
const deltalog = action.deltalog || [];
this.log = [...this.log, ...deltalog];
break;
}

case Actions.SYNC: {
this.log = action.log || [];
break;
}
}

return result;
};

if (enhancer !== undefined) {
enhancer = compose(applyMiddleware(LogMiddleware), enhancer);
} else {
enhancer = applyMiddleware(LogMiddleware);
}

if (multiplayer) {
this.multiplayerClient = new Multiplayer({
Expand Down Expand Up @@ -199,7 +248,7 @@ class _ClientImpl {
const G = this.game.playerView(state.G, state.ctx, this.playerID);

// Combine into return value.
let ret = { ...state, isActive, G };
let ret = { ...state, isActive, G, log: this.log };

if (this.multiplayerClient) {
const isConnected = this.multiplayerClient.isConnected;
Expand Down
60 changes: 59 additions & 1 deletion src/client/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
createEventDispatchers,
createMoveDispatchers,
} from './client';
import { gameEvent } from '../core/action-creators';
import { update, sync, makeMove, gameEvent } from '../core/action-creators';
import Game from '../core/game';
import { RandomBot } from '../ai/bot';

Expand Down Expand Up @@ -268,3 +268,61 @@ describe('move dispatchers', () => {
expect(store.getState().G).toMatchObject({ moved: null });
});
});

describe('log handling', () => {
let client = null;

beforeEach(() => {
client = Client({
game: Game({
moves: {
A: () => ({}),
},
}),
});
});

test('regular', () => {
client.moves.A();
client.moves.A();

expect(client.log).toEqual([
makeMove('A', [], '0'),
makeMove('A', [], '0'),
]);
});

test('update', () => {
const state = { restore: true };
const deltalog = ['0', '1'];
const action = update(state, deltalog);

client.store.dispatch(action);
client.store.dispatch(action);

expect(client.log).toEqual([...deltalog, ...deltalog]);
});

test('sync', () => {
const state = { restore: true };
const log = ['0', '1'];
const action = sync(state, log);

client.store.dispatch(action);
client.store.dispatch(action);

expect(client.log).toEqual(log);
});

test('update - log missing', () => {
const action = update();
client.store.dispatch(action);
expect(client.log).toEqual([]);
});

test('sync - log missing', () => {
const action = sync();
client.store.dispatch(action);
expect(client.log).toEqual([]);
});
});
4 changes: 2 additions & 2 deletions src/client/debug/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Controls } from './controls';
import { PlayerInfo } from './playerinfo';
import { DebugMove } from './debug-move';
import { GameLog } from '../log/log';
import { restore } from '../../core/action-creators';
import { sync } from '../../core/action-creators';
import { parse, stringify } from 'flatted';
import './debug.css';

Expand Down Expand Up @@ -124,7 +124,7 @@ export class Debug extends React.Component {
const gamestateJSON = window.localStorage.getItem('gamestate');
if (gamestateJSON !== null) {
const gamestate = parse(gamestateJSON);
this.props.store.dispatch(restore(gamestate));
this.props.store.dispatch(sync(gamestate));
}
};

Expand Down
18 changes: 9 additions & 9 deletions src/client/debug/debug.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import React from 'react';
import { stringify } from 'flatted';
import { restore, makeMove, gameEvent } from '../../core/action-creators';
import { Client } from '../client';
import { sync } from '../../core/action-creators';
import Game from '../../core/game';
import { CreateGameReducer } from '../../core/reducer';
import { createStore } from 'redux';
import { Debug } from './debug';
import Mousetrap from 'mousetrap';
Expand Down Expand Up @@ -110,7 +110,7 @@ describe('save / restore', () => {
test('restore', () => {
Mousetrap.simulate('3');
expect(getItem).toHaveBeenCalled();
expect(loggedAction).toEqual(restore(restoredState));
expect(loggedAction).toEqual(sync(restoredState));
});

test('restore from nothing does nothing', () => {
Expand Down Expand Up @@ -151,16 +151,16 @@ describe('log', () => {
A: (G, ctx, arg) => ({ arg }),
},
});
const reducer = CreateGameReducer({ game });
let state = reducer(undefined, { type: 'init' });
state = reducer(state, makeMove('A', [42]));
state = reducer(state, gameEvent('endTurn'));

const client = Client({ game });
client.moves.A(42);
client.events.endTurn();

const debug = Enzyme.mount(
<Debug
overrideGameState={overrideGameState}
reducer={reducer}
gamestate={state}
reducer={client.reducer}
gamestate={client.getState()}
endTurn={() => {}}
gameID="default"
/>
Expand Down
24 changes: 12 additions & 12 deletions src/client/log/log.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React from 'react';
import { Client } from '../client';
import { makeMove, gameEvent } from '../../core/action-creators';
import Game from '../../core/game';
import { GameLog } from './log';
Expand Down Expand Up @@ -73,32 +74,31 @@ describe('time travel', () => {
},
});

const reducer = CreateGameReducer({ game });
let state = reducer(undefined, { type: 'init' });
const initialState = state;
const client = Client({ game });
const initialState = client.getState()._initial;

state = reducer(state, makeMove('A', [1]));
state = reducer(state, gameEvent('endTurn'));
// Also ends turn automatically.
state = reducer(state, makeMove('A', [42]));
state = reducer(state, makeMove('A', [2]));
state = reducer(state, gameEvent('endTurn'));
client.moves.A(1);
client.events.endTurn();
// Also ends the turn automatically.
client.moves.A(42);
client.moves.A(2);
client.events.endTurn();

let hoverState = null;

const root = Enzyme.mount(
<GameLog
log={state.log}
log={client.log}
initialState={initialState}
onHover={({ state }) => {
hoverState = state;
}}
reducer={reducer}
reducer={client.reducer}
/>
);

test('before rewind', () => {
expect(state.G).toMatchObject({ arg: 2 });
expect(client.getState().G).toMatchObject({ arg: 2 });
});

test('regular move', () => {
Expand Down
29 changes: 14 additions & 15 deletions src/client/multiplayer/multiplayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@
* https://opensource.org/licenses/MIT.
*/

import { RESTORE } from '../../core/action-types';
import * as ActionCreators from '../../core/action-creators';
import { createStore, applyMiddleware, compose } from 'redux';
import io from 'socket.io-client';

// The actions that are sent across the network.
const blacklistedActions = new Set([RESTORE]);

/**
* Multiplayer
*
Expand Down Expand Up @@ -66,9 +62,9 @@ export class Multiplayer {
const state = getState();
const result = next(action);

if (!blacklistedActions.has(action.type) && action._remote != true) {
if (action.clientOnly != true) {
this.socket.emit(
'action',
'update',
action,
state._stateID,
this.gameID,
Expand Down Expand Up @@ -102,13 +98,18 @@ export class Multiplayer {
}
}

this.socket.on('sync', (gameID, state) => {
if (
gameID == this.gameID &&
state._stateID >= this.store.getState()._stateID
) {
const action = ActionCreators.restore(state);
action._remote = true;
this.socket.on('update', (gameID, state, deltalog) => {
const currentState = this.store.getState();

if (gameID == this.gameID && state._stateID >= currentState._stateID) {
const action = ActionCreators.update(state, deltalog);
this.store.dispatch(action);
}
});

this.socket.on('sync', (gameID, state, log) => {
if (gameID == this.gameID) {
const action = ActionCreators.sync(state, log);
this.store.dispatch(action);
}
});
Expand Down Expand Up @@ -142,7 +143,6 @@ export class Multiplayer {
this.gameID = this.gameName + ':' + id;

const action = ActionCreators.reset();
action._remote = true;
this.store.dispatch(action);

if (this.socket) {
Expand All @@ -158,7 +158,6 @@ export class Multiplayer {
this.playerID = id;

const action = ActionCreators.reset();
action._remote = true;
this.store.dispatch(action);

if (this.socket) {
Expand Down
Loading

0 comments on commit 500ecc3

Please sign in to comment.