From 2de16654269caa1a7b825b2dca54a2c337c2d7ae Mon Sep 17 00:00:00 2001 From: cha0s Date: Sun, 21 Jun 2020 09:47:44 -0500 Subject: [PATCH] feat: redux, entities --- package.json | 2 + src/client/app.jsx | 2 +- src/client/index.jsx | 11 +++- .../{component/index.jsx => entity.jsx} | 18 ++++-- .../index.raw.scss => entity.raw.scss} | 0 .../resources/entity/entity.resource.js | 4 +- src/client/resources/entity/slice.js | 0 src/client/resources/entity/state.js | 59 +++++++++++++++++++ .../entity/{component => }/traits.jsx | 0 .../entity/{component => }/traits.raw.scss | 0 src/client/store.js | 25 ++++++++ src/common/store/effects.js | 2 + src/common/store/index.js | 18 ++++++ src/common/store/middleware.js | 16 +++++ src/server/app.js | 3 + src/server/routes/create.js | 5 ++ yarn.lock | 25 +++++++- 17 files changed, 182 insertions(+), 8 deletions(-) rename src/client/resources/entity/{component/index.jsx => entity.jsx} (56%) rename src/client/resources/entity/{component/index.raw.scss => entity.raw.scss} (100%) delete mode 100644 src/client/resources/entity/slice.js create mode 100644 src/client/resources/entity/state.js rename src/client/resources/entity/{component => }/traits.jsx (100%) rename src/client/resources/entity/{component => }/traits.raw.scss (100%) create mode 100644 src/client/store.js create mode 100644 src/common/store/effects.js create mode 100644 src/common/store/index.js create mode 100644 src/common/store/middleware.js create mode 100644 src/server/routes/create.js diff --git a/package.json b/package.json index 2b2414b..c89173a 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,13 @@ "html-entities": "1.3.1", "immer": "^7.0.1", "memorystore": "^1.6.2", + "normalizr": "^3.6.0", "prop-types": "^15", "react": "16.8.6", "react-dom": "16.8.6", "react-hot-loader": "^4.12.21", "react-markdown": "^4.3.1", + "react-redux": "^7.2.0", "react-tabs": "^3.1.1", "scwp": "1.x", "socket.io-redis": "^5.3.0", diff --git a/src/client/app.jsx b/src/client/app.jsx index 6c530b7..38f520f 100644 --- a/src/client/app.jsx +++ b/src/client/app.jsx @@ -16,7 +16,7 @@ const App = () => { const [typeRenderers] = useState(typeRenderMap()); return ( - + ); }; diff --git a/src/client/index.jsx b/src/client/index.jsx index dd1fec4..9484a83 100644 --- a/src/client/index.jsx +++ b/src/client/index.jsx @@ -3,9 +3,18 @@ import './index.scss'; import {enableMapSet} from 'immer'; import React from 'react'; import {render} from 'react-dom'; +import {Provider} from 'react-redux'; import App from './app'; +import createStore from './store'; enableMapSet(); -render(, document.getElementById('root')); +render( + ( + + + + ), + document.getElementById('root'), +); diff --git a/src/client/resources/entity/component/index.jsx b/src/client/resources/entity/entity.jsx similarity index 56% rename from src/client/resources/entity/component/index.jsx rename to src/client/resources/entity/entity.jsx index 317b562..0bb796c 100644 --- a/src/client/resources/entity/component/index.jsx +++ b/src/client/resources/entity/entity.jsx @@ -1,18 +1,28 @@ import {compose} from '@avocado/core'; import {Entity} from '@avocado/entity'; import contempo from 'contempo'; -import React from 'react'; +import React, {useEffect} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {fetchEntity, selectEntityByUri} from './state'; import Traits from './traits'; const decorate = compose( - contempo(require('./index.raw.scss')), + contempo(require('./entity.raw.scss')), ); -const json = require('~/../fixtures/rock-projectile.entity.json'); - const EntityComponent = (props) => { const {uri} = props; + const dispatch = useDispatch(); + const json = useSelector((state) => selectEntityByUri(state, uri)); + useEffect(() => { + if (!json) { + dispatch(fetchEntity(uri)); + } + }, [json, uri]); + if (!json) { + return null; + } const entity = new Entity(json); const {traits} = json; return ( diff --git a/src/client/resources/entity/component/index.raw.scss b/src/client/resources/entity/entity.raw.scss similarity index 100% rename from src/client/resources/entity/component/index.raw.scss rename to src/client/resources/entity/entity.raw.scss diff --git a/src/client/resources/entity/entity.resource.js b/src/client/resources/entity/entity.resource.js index fbabc52..923bf99 100644 --- a/src/client/resources/entity/entity.resource.js +++ b/src/client/resources/entity/entity.resource.js @@ -1,6 +1,8 @@ -import EntityComponent from './component'; +import EntityComponent from './entity'; export default { + keys: ['entity', 'entities'], matcher: /\.entity\.json$/, Component: EntityComponent, + state: require('./state'), }; diff --git a/src/client/resources/entity/slice.js b/src/client/resources/entity/slice.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/client/resources/entity/state.js b/src/client/resources/entity/state.js new file mode 100644 index 0000000..173d2b8 --- /dev/null +++ b/src/client/resources/entity/state.js @@ -0,0 +1,59 @@ +import {normalize, schema} from 'normalizr'; +import { + createAsyncThunk, + createEntityAdapter, + createSelector, + createSlice, +} from '@reduxjs/toolkit'; + +const entitySchema = new schema.Entity('entities', {}, {idAttribute: 'uuid'}); + +export const fetchEntity = createAsyncThunk( + 'entities/fetchEntity', + async (uri) => { + const data = await fetch(uri); + const json = await data.json(); + const {entities} = normalize(json, entitySchema); + return entities; + } +); + +const adapter = createEntityAdapter({ + selectId: (entity) => entity.uuid, + sortComparer: (a, b) => a.uuid.localeCompare(b.uuid), +}); + +export const { + selectById: selectEntityById, + selectIds: selectEntityIds, + selectEntities: selectEntityEntities, + selectAll: selectAllEntities, + selectTotal: selectTotalEntities +} = adapter.getSelectors((state) => state.entities) + +export const selectEntityUris = createSelector( + (state) => state.entities, + (entities) => entities.uris, +); + +export const selectEntityByUri = createSelector( + (state) => state.entities, + selectEntityUris, + (_, uri) => uri, + ({entities}, uris, uri) => entities[uris[uri]], +) + +const slice = createSlice({ + name: 'entities', + initialState: adapter.getInitialState({uris: {}}), + reducers: {}, + extraReducers: { + [fetchEntity.fulfilled]: (state, action) => { + adapter.upsertMany(state, action.payload.entities); + Object.entries(action.payload.entities) + .forEach(([id, entity]) => state.uris[entity.uri] = id); + }, + }, +}); + +export default slice.reducer; diff --git a/src/client/resources/entity/component/traits.jsx b/src/client/resources/entity/traits.jsx similarity index 100% rename from src/client/resources/entity/component/traits.jsx rename to src/client/resources/entity/traits.jsx diff --git a/src/client/resources/entity/component/traits.raw.scss b/src/client/resources/entity/traits.raw.scss similarity index 100% rename from src/client/resources/entity/component/traits.raw.scss rename to src/client/resources/entity/traits.raw.scss diff --git a/src/client/store.js b/src/client/store.js new file mode 100644 index 0000000..4d5a750 --- /dev/null +++ b/src/client/store.js @@ -0,0 +1,25 @@ +import merge from 'deepmerge'; + +import {combineReducers} from 'redux'; + +import createCommonStore from '~/common/store'; + +import {all} from './resources.scwp'; + +const resources = Object.values(all()).map((M) => M.default); +const reducerMap = resources.reduce( + (r, {keys, state: {'default': reducer}}) => ({...r, [keys[1]]: reducer}), + {} +); + +export default function createStore(options = {}) { + return createCommonStore( + merge( + options, + { + middleware: [], + reducer: combineReducers(reducerMap), + }, + ), + ); +} diff --git a/src/common/store/effects.js b/src/common/store/effects.js new file mode 100644 index 0000000..02a61be --- /dev/null +++ b/src/common/store/effects.js @@ -0,0 +1,2 @@ +export default { +}; diff --git a/src/common/store/index.js b/src/common/store/index.js new file mode 100644 index 0000000..ef3954f --- /dev/null +++ b/src/common/store/index.js @@ -0,0 +1,18 @@ +import merge from 'deepmerge'; +import {configureStore, getDefaultMiddleware} from '@reduxjs/toolkit'; + +import commonMiddleware from './middleware'; + +export default function createStore(options = {}) { + return configureStore( + merge( + { + middleware: [ + ...getDefaultMiddleware(), + commonMiddleware, + ], + }, + options, + ), + ); +} diff --git a/src/common/store/middleware.js b/src/common/store/middleware.js new file mode 100644 index 0000000..55baa48 --- /dev/null +++ b/src/common/store/middleware.js @@ -0,0 +1,16 @@ +import effects from './effects'; + +const debug = require('debug')('persea:common:store'); + +export default (store) => (next) => (action) => { + const {meta, payload} = action; + debug("action '%s' dispatched: %o", action.type, { + payload, + meta, + }); + const result = next(action); + if (effects[action.type]) { + setTimeout(() => effects[action.type](store, action), 0); + } + return result; +}; diff --git a/src/server/app.js b/src/server/app.js index cf02243..db296c7 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -1,6 +1,8 @@ import express from 'express'; import httpSession from 'express-session'; +import createRoutes from './routes/create'; + const MemoryStore = require('memorystore')(httpSession); let insideSession; @@ -19,6 +21,7 @@ export default function createApp() { }), }); app.use(insideSession); + createRoutes(app); return app; } diff --git a/src/server/routes/create.js b/src/server/routes/create.js new file mode 100644 index 0000000..9bd7e0a --- /dev/null +++ b/src/server/routes/create.js @@ -0,0 +1,5 @@ +import express from 'express'; + +export default function createRoutes(app) { + app.use('/resources', express.static('resources')); +} diff --git a/yarn.lock b/yarn.lock index 9f6c8f3..1c1ee28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -989,6 +989,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.5.5": + version "7.10.3" + resolved "https://npm.i12e.cha0s.io/@babel%2fruntime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.1": version "7.10.1" resolved "https://npm.i12e.cha0s.io/@babel%2ftemplate/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811" @@ -6598,6 +6605,11 @@ normalize-url@1.9.1: query-string "^4.1.0" sort-keys "^1.0.0" +normalizr@^3.6.0: + version "3.6.0" + resolved "https://npm.i12e.cha0s.io/normalizr/-/normalizr-3.6.0.tgz#b8bbc4546ffe43c1c2200503041642915fcd3e1c" + integrity sha512-25cd8DiDu+pL46KIaxtVVvvEPjGacJgv0yUg950evr62dQ/ks2JO1kf7+Vi5/rMFjaSTSTls7aCnmRlUSljtiA== + notepack.io@~2.1.2: version "2.1.3" resolved "https://npm.i12e.cha0s.io/notepack.io/-/notepack.io-2.1.3.tgz#cc904045c751b1a27b2dcfd838d81d0bf3ced923" @@ -7601,7 +7613,7 @@ react-hot-loader@^4.12.21: shallowequal "^1.1.0" source-map "^0.7.3" -react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6: +react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://npm.i12e.cha0s.io/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7625,6 +7637,17 @@ react-markdown@^4.3.1: unist-util-visit "^1.3.0" xtend "^4.0.1" +react-redux@^7.2.0: + version "7.2.0" + resolved "https://npm.i12e.cha0s.io/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d" + integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA== + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-tabs@^3.1.1: version "3.1.1" resolved "https://npm.i12e.cha0s.io/react-tabs/-/react-tabs-3.1.1.tgz#b363a239f76046bb2158875a1e5921b11064052f"