From 8d7614bcaf01c11b7f096c5af1e138623003deca Mon Sep 17 00:00:00 2001 From: Kasper Sebb' brandt Date: Sat, 17 Aug 2019 16:08:46 +0200 Subject: [PATCH] Refactor to use Redux library --- package.json | 2 + src/actions.js | 59 ---------------------- src/connection.reducer.js | 20 -------- src/effects.js | 39 --------------- src/public.reducer.js | 38 -------------- src/store/connection.reducer.js | 30 +++++++++++ src/store/effects.js | 46 +++++++++++++++++ src/store/effects.middlware.js | 9 ++++ src/store/table.reducer.js | 46 +++++++++++++++++ src/store/table.store.js | 26 ++++++++++ src/table.js | 89 +++++++++++++++------------------ test.js | 27 +++++----- yarn.lock | 20 +++++++- 13 files changed, 229 insertions(+), 222 deletions(-) delete mode 100644 src/actions.js delete mode 100644 src/connection.reducer.js delete mode 100644 src/effects.js delete mode 100644 src/public.reducer.js create mode 100644 src/store/connection.reducer.js create mode 100644 src/store/effects.js create mode 100644 src/store/effects.middlware.js create mode 100644 src/store/table.reducer.js create mode 100644 src/store/table.store.js diff --git a/package.json b/package.json index 43d927d..086c445 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "koa": "^2.7.0", "koa-router": "^7.4.0", "koa-websocket": "^5.0.1", + "redux": "^4.0.4", + "redux-thunk": "^2.3.0", "uuid": "^3.3.2" }, "devDependencies": { diff --git a/src/actions.js b/src/actions.js deleted file mode 100644 index 28b944e..0000000 --- a/src/actions.js +++ /dev/null @@ -1,59 +0,0 @@ -function dispatchMessage(msg, collectionName, subscriptions, ignore = []) { - if (subscriptions[collectionName]) { - subscriptions[collectionName].forEach((sock) => { - if (ignore && ignore.indexOf(sock) === -1) { - sock.send(JSON.stringify(msg)); - } - }); - } -} - -function handleDisconnect(collectionName, dbi, sock, message) {} - -const actions = { - addAction: async (collectionName, dbi, sock, req, subs) => { - const resData = await dbi.create(collectionName, req); - dispatchMessage({ - action: req.action, - data: resData - }, collectionName, subs); - }, - updateAction: async (collectionName, dbi, sock, req, subs) => { - const method = Array.isArray(req.data) ? 'updateMany' : 'updateOne'; - const resData = await dbi[method](collectionName, req); - dispatchMessage({ - action: req.action, - data: resData - }, collectionName, subs); - }, - deleteAction: async (collectionName, dbi, sock, req, subs) => { - const resData = await dbi.delete(collectionName, req); - dispatchMessage({ - action: req.action, - data: { - where: req.where - } - }, collectionName, subs); - }, - pingAction: async (collectionName, dbi, sock, req, subs) => { - sock.send(JSON.stringify({ - action: req.action, - data: { - servertime: performance.now(), - clienttime: req.clienttime - } - })); - }, - subAction: (collectionName, dbi, sock, message, subs) => { - if (!subs[collectionName]) { - subs[collectionName] = []; - } - subs[collectionName].push(sock); - }, - unsubAction: (collectionName, dbi, sock, message, subs) => { - if (!subs[collectionName]) { - subs[collectionName] = []; - } - subs[collectionName] = subs[collectionName].filter((s) => s != sock); - } -} diff --git a/src/connection.reducer.js b/src/connection.reducer.js deleted file mode 100644 index 1ff7eac..0000000 --- a/src/connection.reducer.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = function(state, action) { - switch(action.type) { - case 'open': { - return { - ...state, - sockets: [...state.sockets, action.data.socket.uuid] - } - } - case 'close': { - action.data.socket.terminate(); - - return { - ...state, - sockets: state.sockets.filter(a => a !== action.data.socket.uuid) - } - } - } - - return state; -} \ No newline at end of file diff --git a/src/effects.js b/src/effects.js deleted file mode 100644 index fa0a92f..0000000 --- a/src/effects.js +++ /dev/null @@ -1,39 +0,0 @@ -const { performance } = require('perf_hooks'); - -module.exports = { - publicEffect: ({ action, dispatch, getState, websocket, ws}) => { - switch(action.type) { - case 'open': - case 'public:current': - return websocket.send(JSON.stringify({ - type: 'public:set', - data: getState().clientState, - time: performance.now() - })); - case 'public:add': - case 'public:set': - return ws.server.clients.forEach(function each(client) { - if(client !== websocket) { - client.send(JSON.stringify({ ...action, time: performance.now() })); - } - }); - case 'public:add:all': - case 'public:set:all': - return ws.server.clients.forEach(function each(client) { - client.send(JSON.stringify({ ...action, time: performance.now() })); - }); - } - }, - timesyncEffect: ({ action, dispatch, getState, websocket, ws}) => { - switch(action.type) { - case 'get:time': { - websocket.send(JSON.stringify({ - type: 'get:time', - data: { - time: performance.now() - } - })); - } - } - } -}; \ No newline at end of file diff --git a/src/public.reducer.js b/src/public.reducer.js deleted file mode 100644 index 2584944..0000000 --- a/src/public.reducer.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = function(state, action) { - switch(action.type) { - case 'public:add': - case 'public:add:all': - const clientState = { - ...state.clientState - } - - Object.keys(action.data).forEach(k => { - if (clientState[k] && Array.isArray(clientState[k])) { - clientState[k] = [ - ...clientState[k], - ...action.data[k] - ]; - } else { - clientState[k] = typeof(clientState[k]) === 'object' - ? { ...clientState[k], ...action.data[k] } - : action.data[k]; - } - }); - - return { - ...state, - clientState - } - case 'public:set': - case 'public:set:all': - return { - ...state, - clientState: { - ...state.clientState, - ...action.data - } - } - default: - return state; - } -} \ No newline at end of file diff --git a/src/store/connection.reducer.js b/src/store/connection.reducer.js new file mode 100644 index 0000000..e17ddcb --- /dev/null +++ b/src/store/connection.reducer.js @@ -0,0 +1,30 @@ +const connectionActions = { + OPEN: 'connection:open', + CLOSE: 'connection:close' +} + +function connection(state = { sockets: [] }, action) { + switch(action.type) { + case connectionActions.OPEN: { + return { + ...state, + sockets: [...state.sockets, action.socket.uuid] + } + } + case connectionActions.CLOSE: { + action.data.socket.terminate(); + + return { + ...state, + sockets: state.sockets.filter(a => a !== action.data.socket.uuid) + } + } + } + + return state; +} + +module.exports = { + connectionActions, + connection +} \ No newline at end of file diff --git a/src/store/effects.js b/src/store/effects.js new file mode 100644 index 0000000..7a29072 --- /dev/null +++ b/src/store/effects.js @@ -0,0 +1,46 @@ +const { performance } = require('perf_hooks'); +const { tableActions } = require('./table.reducer'); +const { connectionActions } = require('./connection.reducer'); + +const timesyncActions = { + GET_TIME: 'get:time' +}; + +module.exports = { + timesyncActions, + tableEffect: ({ action, dispatch, getState, ws}) => { + switch(action.type) { + case connectionActions.OPEN: + case tableActions.CURRENT: + return action.socket.send(JSON.stringify({ + type: tableActions.SET, + data: getState().clientState, + time: performance.now() + })); + case tableActions.ADD: + case tableActions.SET: + return ws.server.clients.forEach(function each(client) { + if(client !== action.socket) { + client.send(JSON.stringify({ ...action, time: performance.now() })); + } + }); + case tableActions.ADD_ALL: + case tableActions.SET_ALL: + return ws.server.clients.forEach(function each(client) { + client.send(JSON.stringify({ ...action, time: performance.now() })); + }); + } + }, + timesyncEffect: ({ action, dispatch, getState, ws}) => { + switch(action.type) { + case timesyncActions.GET_TIME: { + action.socket.send(JSON.stringify({ + type: timesyncActions.GET_TIME, + data: { + time: performance.now() + } + })); + } + } + } +}; \ No newline at end of file diff --git a/src/store/effects.middlware.js b/src/store/effects.middlware.js new file mode 100644 index 0000000..d6b1ed9 --- /dev/null +++ b/src/store/effects.middlware.js @@ -0,0 +1,9 @@ +function createEffectsMiddleware(effects = [], ws) { + return ({ dispatch, getState }) => next => action => { + const n = next(action); + effects.forEach(e => e({action, dispatch, getState, ws})); + return n; + }; + } + +module.exports = createEffectsMiddleware; \ No newline at end of file diff --git a/src/store/table.reducer.js b/src/store/table.reducer.js new file mode 100644 index 0000000..84b6081 --- /dev/null +++ b/src/store/table.reducer.js @@ -0,0 +1,46 @@ +const tableActions = { + ADD: 'public:add', + ADD_ALL: 'public:add:all', + CURRENT: 'public:current', + SET: 'public:set', + SET_ALL: 'public:set:all', +} + +function createTableReducer(prefix = '') { + return function(state = {}, action) { + switch(action.type) { + case prefix+tableActions.ADD: + case prefix+tableActions.ADD_ALL: + const newState = { ...state } + + Object.keys(action.data).forEach(k => { + if (newState[k] && Array.isArray(newState[k])) { + newState[k] = [ + ...newState[k], + ...action.data[k] + ]; + } else { + newState[k] = typeof(newState[k]) === 'object' + ? { ...newState[k], ...action.data[k] } + : action.data[k]; + } + }); + + return { ...state, newState } + case prefix+tableActions.SET: + case prefix+tableActions.SET_ALL: + return { + ...state, + ...action.data + } + default: + return state; + } + } +} + +module.exports = { + table: createTableReducer(), + createTableReducer, + tableActions +} \ No newline at end of file diff --git a/src/store/table.store.js b/src/store/table.store.js new file mode 100644 index 0000000..ebc149a --- /dev/null +++ b/src/store/table.store.js @@ -0,0 +1,26 @@ +const { createStore, applyMiddleware, combineReducers } = require('redux'); +const thunk = require('redux-thunk'); + +const effectsMiddlware = require('./effects.middlware'); + +const { tableEffect, timesyncEffect } = require('./effects'); +const { connection } = require('./connection.reducer'); +const { table } = require('./table.reducer'); + +module.exports = function({ customReducers = {}, initialState = {}, effects = [], ws }) { + let currentAppState = { ...initialState }; + + const allEffects = [ + ...effects, + tableEffect, + timesyncEffect + ] + + const rootReducer = combineReducers({ + connection, + table, + ...customReducers + }); + + return createStore(rootReducer, currentAppState, applyMiddleware(thunk.default, effectsMiddlware(allEffects, ws))); +} diff --git a/src/table.js b/src/table.js index f3b6073..fb74fb4 100644 --- a/src/table.js +++ b/src/table.js @@ -3,70 +3,54 @@ const Router = require('koa-router'); const websockify = require('koa-websocket'); const uuid = require("uuid/v4"); -const connectionReducer = require('./connection.reducer'); -const publicReducer = require('./public.reducer'); -const { publicEffect, timesyncEffect } = require('./effects'); +const { connectionActions } = require('./store/connection.reducer') +const { tableActions } = require('./store/table.reducer'); +const { timesyncActions } = require('./store/effects'); +const tableStore = require('./store/table.store'); + +// @TODO, Table domains // @TODO, How to auth? // @TODO, Binary data events? -// @TODO, Table domains -// @TODO, Rename public to table -// @TODO, Initial state sharing + // @TODO, Reducer sharing??? // @TODO, API exploration, how? -module.exports = function ({ initialState = {}, customReducer = a => a, effects = [], pingInterval = 30000 }) { + +const allowedActions = [ // @TODO allowed actions + ...Object.values(tableActions), + ...Object.values(timesyncActions) +]; + +module.exports = function ({ initialState = {}, reducers = {}, effects = [], pingInterval = 30000 }) { const router = new Router(); const app = websockify(new Koa()); let serverInstance; - effects.push(publicEffect); - effects.push(timesyncEffect); - - let currentAppState = { - ...initialState, - sockets: [] - }; - const dispatchedActions = []; - - function getState() { - return currentAppState; - } - - function updateState(state) { - if (getState() !== state) { - currentAppState = state; - } - } - - function dispatch(action, websocket) { - dispatchedActions.push(action); - const currentState = getState(); - - updateState(connectionReducer(getState(), action)); - updateState(publicReducer(getState(), action)); - updateState(customReducer(getState(), action)); - - //if (currentState !== getState()) { - effects.forEach(e => e({ action, dispatch, getState, websocket, ws: app.ws })); - //} - } + const store = tableStore({ initialState, reducers, effects, ws: app.ws }) router.get(`/`, ctx => { ctx.websocket.uuid = uuid(); - dispatch({ - type: 'open', - data: { socket: ctx.websocket} - }, ctx.websocket); + store.dispatch({ + type: connectionActions.OPEN, + socket: ctx.websocket + }); ctx.websocket.on('message', (rawReq) => { const req = JSON.parse(rawReq); - dispatch({ - type: req.type ? req.type : 'message', - data: req.data ? req.data : rawReq, - }, ctx.websocket); + const allowed = allowedActions.reduce((acc, curr) => { + return acc || (req.type.indexOf(curr) > -1) + }, false) + + if(allowed) { + store.dispatch({ + type: req.type ? req.type : 'message', + data: req.data ? req.data : rawReq, + socket: ctx.websocket + }); + } }); ctx.websocket.on('pong', () => { @@ -74,18 +58,22 @@ module.exports = function ({ initialState = {}, customReducer = a => a, effects }); ctx.websocket.on('close', () => { - return dispatch({ type: 'close', data:{ socket: ctx.websocket } }, ctx.websocket);; + return store.dispatch({ + type: connectionActions.CLOSE, + data: { socket: ctx.websocket }, + socket: ctx.websocket + }); }); }); - const interval = setInterval(function ping() { + const interval = setInterval(() => { if (!app.ws.server) { return; } app.ws.server.clients.forEach(function each(ws) { if (ws.isAlive === false) { - return dispatch({ type: 'close', data:{ socket: ctx.websocket } }, ctx.websocket);; + return store.dispatch({ type: 'close', data: { socket: ws } }); } ws.isAlive = false; @@ -100,9 +88,10 @@ module.exports = function ({ initialState = {}, customReducer = a => a, effects function listen(port) { serverInstance = app.listen(port); + return serverInstance; } app.ws.use(router.routes()); - return { ws: app.ws, server: app, dispatch, getState, kill, listen }; + return { server: app, store, kill, listen }; } \ No newline at end of file diff --git a/test.js b/test.js index 496acff..4a8c346 100644 --- a/test.js +++ b/test.js @@ -2,7 +2,7 @@ const Agent = require('./src/agent'); const createTable = require('./src/table'); const initialState = { - clientState: { + table: { cakes: [ {type: 'cheese', name: 'chuck'}, {type: 'flower', name: 'mac'} @@ -27,10 +27,10 @@ describe(`Test table`, () => { beforeAll(async () => { table = createTable({ initialState, customReducer: appReducer, effects, pingInterval: 500 }); - listenRef = table.listen(8500); + listenRef = table.listen(3333); - agent01 = new Agent("ws://localhost:8500", ["protocolOne", "protocolTwo"]); - agent02 = new Agent("ws://localhost:8500", ["protocolOne", "protocolTwo"]); + agent01 = new Agent("ws://localhost:3333", ["protocolOne", "protocolTwo"]); + agent02 = new Agent("ws://localhost:3333", ["protocolOne", "protocolTwo"]); }); afterEach(() => { @@ -43,14 +43,9 @@ describe(`Test table`, () => { }); it('Agents can connect', done => { - let a = 0; - testEffect = ({ action, dispatch, getState, websocket, ws}) => { - expect(getState().sockets[0]).not.toEqual(undefined); - a++ && a == 2 && done(); - }; - agent01.connect().then(() => { - a++ && a == 2 && done(); + expect(table.store.getState().connection.sockets[0]).not.toEqual(undefined); + done(); }); }); @@ -58,8 +53,8 @@ describe(`Test table`, () => { agent01.connect().then(() => { testEffect = ({ action, dispatch, getState, websocket, ws}) => { if(action.type === 'public:set:all') { - expect(getState().clientState.helloworld).toEqual(true); - expect(getState().clientState.abc).toEqual('cake is great'); + expect(getState().table.helloworld).toEqual(true); + expect(getState().table.abc).toEqual('cake is great'); done(); } @@ -81,6 +76,7 @@ describe(`Test table`, () => { expect(pmsg.type).toEqual('public:set'); done(); } + agent01.connect(); }); @@ -215,11 +211,11 @@ describe(`Test table`, () => { it('Will ping clients to ensure active connection', done => { agent01.connect().then(() => { - expect(table.ws.server.clients.size).toEqual(1) + expect(table.server.ws.server.clients.size).toEqual(1) agent01.destroy(); setTimeout(() => { - expect(table.ws.server.clients.size).toEqual(0) + expect(table.server.ws.server.clients.size).toEqual(0) done(); }, 1000) }).catch(e => console.log('Error:', e)) @@ -262,4 +258,5 @@ describe(`Test table`, () => { agent01.send(originalMsg); }).catch(e => console.log('Error:', e)); }); + }); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 474b685..df027ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2322,7 +2322,7 @@ lodash@^4.17.11: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3002,6 +3002,19 @@ realpath-native@^1.1.0: dependencies: util.promisify "^1.0.0" +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + +redux@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" + integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -3444,6 +3457,11 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +symbol-observable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"