Skip to content
This repository has been archived by the owner on Jan 16, 2023. It is now read-only.

Commit

Permalink
Add function for validating actions; bug fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
eitak committed Mar 20, 2016
1 parent e3df489 commit 930617b
Show file tree
Hide file tree
Showing 14 changed files with 174 additions and 127 deletions.
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"presets": ["es2015"],
"plugins": ["syntax-async-functions", "transform-regenerator"]
"plugins": ["syntax-async-functions", "transform-regenerator", "transform-object-rest-spread"]
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
node_modules
lib
coverage
4 changes: 4 additions & 0 deletions .istanbul.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
instrumentation:
root: src
extensions:
- .js
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
src
test
node_modules
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
},
"scripts": {
"test": "mocha --compilers js:babel-register --reporter nyan --recursive",
"test:coverage": "babel-node node_modules/isparta/bin/isparta cover --report html --include-all-sources node_modules/mocha/bin/_mocha -- --reporter dot --recursive",
"compile": "babel -d lib/ src/",
"prepublish": "npm run compile"
},
Expand All @@ -23,9 +24,12 @@
"devDependencies": {
"babel-cli": "^6.4.5",
"babel-plugin-syntax-async-functions": "^6.5.0",
"babel-plugin-transform-object-rest-spread": "^6.6.5",
"babel-polyfill": "^6.6.1",
"babel-preset-es2015": "^6.3.13",
"babel-register": "^6.4.3",
"isparta": "^4.0.0",
"mocha": "^2.4.5",
"should": "^8.1.1"
},
"dependencies": {
Expand Down
23 changes: 11 additions & 12 deletions src/client/client-action-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ class ClientActionManager {
}
}

async applyServerAction(action) {
async applyServerAction({ action, sequenceNumber, clientId }) {
const expectedSequenceNumber = this.sequenceNumber + 1;
const invalidSequenceNumber = action.sequenceNumber !== expectedSequenceNumber;
const invalidSequenceNumber = sequenceNumber !== expectedSequenceNumber;
if (invalidSequenceNumber) {
console.error('Received action with invalid sequence number (expected %d): %j', expectedSequenceNumber, action);
return;
}

this.sequenceNumber = action.sequenceNumber;
this.sequenceNumber = sequenceNumber;

const isKnownAction = this.clientId === action.clientId;
const isKnownAction = this.clientId === clientId;
if (isKnownAction) {
console.log('Received confirmation that our action saved: %j', action);
this.actionsToSave.shift();
Expand All @@ -50,21 +50,20 @@ class ClientActionManager {
transformedActions.transformedClientActions.push(mergedActions[1]);
transformedActions.newAction = mergedActions[0];
return transformedActions;
}, {transformedClientActions: [], newAction: action.action});
}, {transformedClientActions: [], newAction: action});

await this.dispatchAction(Object.assign({}, transformedActions.newAction, {_originatedFromServer: true}));
await this.dispatchAction({...transformedActions.newAction, _originatedFromServer: true});
this.actionsToSave = transformedActions.transformedClientActions;
}

async _saveAction() {
if (this.actionsToSave.length > 0) {
const actionToSave = {
action: this.actionsToSave[0],
sequenceNumber: this.sequenceNumber + 1,
clientId: this.clientId
};
const actionToSave = this.actionsToSave[0];
console.log('Save action: %j', actionToSave);
await this.saveAction(actionToSave);
await this.saveAction({
sequenceNumber: this.sequenceNumber + 1,
action: actionToSave
});
}
}

Expand Down
17 changes: 13 additions & 4 deletions src/client/index.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import ClientActionManager from './client-action-manager'
import { EventEmitter } from 'events'
import createStore from './store/redux'

async function initializeClient(repository, createStore, mergeActions) {
const NEW_ACTION_EVENT = 'new-action';

async function initializeClient({ repository, reducer, additionalEnhancer, mergeActions, isActionValid=() => true}) {
const initialState = await repository.getInitialState();
const clientId = await repository.getClientId();
const sequenceNumber = await repository.getSequenceNumber();

const newClientActionEventEmitter = new EventEmitter();
const store = createStore(initialState, (action) => newClientActionEventEmitter.emit('new-action', action));
const saveAction = (action) => newClientActionEventEmitter.emit(NEW_ACTION_EVENT, action);
const store = createStore({
reducer,
additionalEnhancer,
initialState,
saveAction,
clientId,
isActionValid
});

const actionManager = new ClientActionManager(repository.saveAction.bind(repository),
store.dispatch.bind(store), mergeActions, sequenceNumber, clientId);

repository.onNewActionFromServer(actionManager.applyServerAction.bind(actionManager));
console.log(actionManager);
newClientActionEventEmitter.on('new-action', actionManager.applyClientAction.bind(actionManager));
newClientActionEventEmitter.on(NEW_ACTION_EVENT, actionManager.applyClientAction.bind(actionManager));

return store;
}
Expand Down
5 changes: 3 additions & 2 deletions src/client/repository/socketio.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ class SocketIo {
});
}

saveAction(action) {
this.socket.emit(SocketIoEvents.SAVE_ACTION, action);
saveAction(saveActionRequest) {
this.socket.emit(SocketIoEvents.SAVE_ACTION, saveActionRequest);
return Promise.resolve();
}

async getClientId() {
Expand Down
27 changes: 19 additions & 8 deletions src/client/store/redux.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { createStore, applyMiddleware, compose } from 'redux'
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'

const createReduxState = (reducer, additionalMiddleware) => (initialState, saveAction) => {
const saveActionMiddleware = () => {
function createReduxStore({ reducer, additionalEnhancer, initialState, saveAction, clientId, isActionValid }) {
function reducerWithClientId(state, action) {
return {...reducer(state, action), clientId};
}

const saveActionMiddleware = applyMiddleware((store) => {
return (next) => (action) => {
const state = store.getState();
const validAction = action._originatedFromServer || isActionValid(state, action, clientId);
if (!validAction) {
console.warn('Rejecting action %j as it is not valid for the current state: %j', action, state);
return;
}
const nextAction = next(action);
saveAction(nextAction);
return nextAction;
}
};
const middleware = additionalMiddleware ? compose(additionalMiddleware, saveActionMiddleware) : saveActionMiddleware;
return createStore(reducer, initialState, applyMiddleware(middleware));
};
});

const enhancer = additionalEnhancer ? compose(additionalEnhancer, saveActionMiddleware) : saveActionMiddleware;
return createStore(reducerWithClientId, initialState, enhancer);
}

export default createReduxState;
export default createReduxStore;
21 changes: 10 additions & 11 deletions src/server/client/socketio.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import socketio from 'socket.io'
import SocketIoEvents from '../../shared/events'
import Events from './../events'
import { EventEmitter } from 'events'
import uuid from 'node-uuid'

Expand All @@ -25,27 +24,27 @@ class SocketIoClient {
const snapshot = await db.getSnapshot(stateId);
cb({sequenceNumber: snapshot.sequenceNumber, clientId: clientId, state: snapshot.state});

socket.on(SocketIoEvents.SAVE_ACTION, (action) => {
console.log('Received action from client %s for stateId: %s; action: %j', clientId, stateId, action);
socket.on(SocketIoEvents.SAVE_ACTION, (request) => {
console.log('Received save action request from client %s for stateId: %s; action: %j',
clientId, stateId, request);

const actionWithClientId = Object.assign({}, action, {clientId: clientId});
this._eventEmitter.emit(SAVE_ACTION_EVENT, stateId, actionWithClientId)
const requestWithClientId = Object.assign({}, request, {clientId: clientId});
this._eventEmitter.emit(SAVE_ACTION_EVENT, stateId, requestWithClientId)
});

});
});

}

emitAction(stateId, action) {
console.log('Emitting action for state %s: %j', stateId, action);
this.io.emit(stateId, action);
emitAction(stateId, actionSavedEvent) {
console.log('Emitting action saved event for state %s: %j', stateId, actionSavedEvent);
this.io.emit(stateId, actionSavedEvent);
}

onSaveActionRequest(cb) {
this._eventEmitter.on(SAVE_ACTION_EVENT, (stateId, action) => {
console.log('Got save action request for state %s, action %j', stateId, action);
cb(stateId, action);
this._eventEmitter.on(SAVE_ACTION_EVENT, (stateId, request) => {
cb(stateId, request.clientId, request.sequenceNumber, request.action);
})
}

Expand Down
43 changes: 23 additions & 20 deletions src/server/db/local-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const NEW_ACTION_EVENT = 'new-action';

class LocalDb {

constructor() {
constructor(initialState) {
this._eventEmitter = new EventEmitter();
this._actions = {};
this._snapshots = {};
this.initialState = initialState;
}

deleteState(stateId) {
Expand All @@ -20,14 +21,20 @@ class LocalDb {
if (!snapshot) {
return Promise.resolve({
sequenceNumber: 0,
state: {amount: 0}
state: this.initialState
})
}

return Promise.resolve({
sequenceNumber: this._actions[stateId].length,
state: snapshot
});
return Promise.resolve(snapshot);
}

async clearOldActions(stateId, recordsToKeep=10) {
const snapshot = await this.getSnapshot(stateId);
let sequenceNumber = snapshot.sequenceNumber - recordsToKeep + 1; // keep last 10 actions
while (this._actions[stateId] && this._actions[stateId][sequenceNumber]) {
delete this._actions[stateId][sequenceNumber];
sequenceNumber = sequenceNumber - 1;
}
}

async saveSnapshot(stateId, snapshot) {
Expand All @@ -40,31 +47,27 @@ class LocalDb {
this._snapshots[stateId] = snapshot;
}

getActionBySequenceNumber(stateId, sequenceNumber) {
async getActionBySequenceNumber(stateId, sequenceNumber) {
const actionsForState = this._actions[stateId];
if (!actionsForState) {
return Promise.reject();
throw new Error(`Could not find action with sequence number ${sequenceNumber} and stateId ${stateId}`);
}

if (actionsForState.length < sequenceNumber) {
return Promise.reject();
const snapshot = await this.getSnapshot(stateId);
if (snapshot.sequenceNumber < sequenceNumber) {
throw new Error(`Could not find action with sequence number ${sequenceNumber} and stateId ${stateId}`);
}

return Promise.resolve(actionsForState[sequenceNumber - 1]);
return actionsForState[sequenceNumber];
}

saveAction(stateId, action) {
saveAction(stateId, actionRecord) {
if (!this._actions[stateId]) {
this._actions[stateId] = [];
this._snapshots[stateId] = {amount: 0};
this._actions[stateId] = {};
}

this._actions[stateId].push(action);
var newAmount = this._snapshots[stateId].amount + action.action.amount;
this._snapshots[stateId] = {
amount: newAmount
};
this._eventEmitter.emit(NEW_ACTION_EVENT, stateId, action);
this._actions[stateId][actionRecord.sequenceNumber] = actionRecord;
this._eventEmitter.emit(NEW_ACTION_EVENT, stateId, actionRecord);
return Promise.resolve();
}

Expand Down
5 changes: 3 additions & 2 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import createSaveActionFunction from './save-action'

function initializeServer(db, client, mergeActions) {
const saveAction = createSaveActionFunction(db, mergeActions);
function initializeServer({ db, client, mergeActions, reducer, isActionValid }) {
const saveAction = createSaveActionFunction({ db, mergeActions, reducer, isActionValid });
db.onNewAction(client.emitAction.bind(client));
client.onSaveActionRequest(saveAction);
return saveAction;
}

export default initializeServer;
40 changes: 25 additions & 15 deletions src/server/save-action.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,48 @@
import _ from 'lodash'
import { createStore } from 'redux'

function createSaveActionFunction (db, mergeActions, reducer) {
function createSaveActionFunction ({ db, mergeActions, reducer, isActionValid=() => true }) {

return async function saveAction(stateId, clientId, sequenceNumber, action) {
delete action._originatedFromServer;

return async function saveAction(stateId, action) {
const previousSnapshot = await db.getSnapshot(stateId);
const lastSequenceNumber = previousSnapshot.sequenceNumber;
const nextSequenceNumber = lastSequenceNumber + 1;

const invalidSequenceNumber = action.sequenceNumber < 0 || action.sequenceNumber > nextSequenceNumber;
const invalidSequenceNumber = sequenceNumber <= 0 || sequenceNumber > nextSequenceNumber;
if (invalidSequenceNumber) {
console.error('Invalid sequence number %d', action.sequenceNumber);
throw new Error('Invalid sequence number %d', action.sequenceNumber);
console.error('Invalid sequence number %d', sequenceNumber);
throw new Error('Invalid sequence number %d', sequenceNumber);
}

const currentState = previousSnapshot.state;
const validAction = isActionValid(currentState, action, clientId);
if (!validAction) {
console.error('Action is not valid for stateId %s: %j', stateId, action);
throw new Error('Invalid action %j', action);
}

const serverActions = await Promise.all(_.range(action.sequenceNumber, nextSequenceNumber)
const serverActions = await Promise.all(_.range(sequenceNumber, nextSequenceNumber)
.map((sequenceNumber) => db.getActionBySequenceNumber(stateId, sequenceNumber)));

const actionToSave = serverActions
.reduce((transformedAction, serverAction) => {
const mergedActions = mergeActions(transformedAction, serverAction.action);
const mergedActions = mergeActions(transformedAction, serverAction);
return mergedActions[1];
}, action.action);

console.log('Saving for stateId: %s, clientId: %s, sequenceNumber: %s, action: %j',
stateId, action.clientId, nextSequenceNumber, actionToSave);
}, action);

await db.saveAction(stateId, {
const recordToSave = {
clientId: clientId,
sequenceNumber: nextSequenceNumber,
clientId: action.clientId,
action: actionToSave
});
};

console.log('Saving for stateId: %s, record: %j', stateId, recordToSave);

await db.saveAction(stateId, recordToSave);

const store = createStore(reducer, previousSnapshot.state);
const store = createStore(reducer, currentState);
store.dispatch(actionToSave);

await db.saveSnapshot(stateId, {
Expand Down
Loading

0 comments on commit 930617b

Please sign in to comment.