Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redux #1

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
_build
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# http://editorconfig.org
root = true

[*]
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ fabric.properties

/dist/
/package-lock.json
/_build/
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
sudo: false
language: node_js
node_js:
- 6
cache:
directories:
- node_modules
before_install:
- export TZ=America/Los_Angeles
script:
- npm run ci
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Container for support server
# Usage
# Build: docker build -t derpymon .
# Run: docker run --name derp -p 8888:8888 derpymon
# Share volumes:
# docker run --name derp \
# -v `pwd`/src:/derpymon/src -v `pwd`/tests:/derpymon/tests \
# -p 8888:8888 -p 8001:8001 -p 8010:8010 derpymon
# Shell: docker exec -it derp /bin/bash
# Stop: docker stop derp
FROM node:8

COPY . /derpymon
WORKDIR /derpymon
RUN npm install
RUN npm run build
EXPOSE 8888
EXPOSE 8001
ENTRYPOINT [ "npm", "run", "serve" ]
78 changes: 74 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,83 @@
# Redux Refactor

This branch is part of a purposeful effort to refactor the more traditional store/executor pattern to use a Redux style
pattern with Dojo 2 in its place.

## Approach

Refactoring into a Redux pattern can be an incremental process. The trickiest part is managing the migration from a
traditional store to a Redux store that acts more like a middleware. During Derpymon's transition we'll use the custom
built `inject` and `Container` that allows for multiple injection to allow us to maintain our old store and Redux store
side-by-side. Other approaches could be to move your old store into the Redux store as a non-pure object and migrate
out functionality incrementally or move everything out of the old store into the Redux store. Your preferred approach
likely depends on the number of tests that already exist in your application, how important preventing regressions, and
your willingness to add bridge-frameworks (like the multi-injection `Container`) to support the migration. Whatever your
choice it is important to complete the transition to a Redux pattern. Maintaining two store patterns creates too much
chaos for any benefit it might provide and is not worth keeping around for longer than necessary.

1. Containerize widgets that use injected state
1. Convert commands to stateless functions detached from an executor
1. Update containers to use stateless commands
1. Move event handlers to stateless commands (attachHandler /detachHandler) in preparation for Redux middleware
1. Create the initial state for the application
1. Create reducer(s) to transform state and match features with the old store's transformed states
1. Create actions and action creators to replace stateless functions (commands)
1. Create a ReduxStore, migrate Containers to use it, and remove the old store(s)
1. Refactor event handlers to Redux middleware

## Steps

Derpymon was originally designed as a more traditional command/executor pattern. State was encapsulated in multiple
injectors as a means of compartmentalizing and reducing the scope and complexity of application-wide state. The
consequence of this pattern was a need for commands to be able to gather state from multiple sources and potentially
change state at multiple sources. This spreads responsibility for changing state out to the edges of the application
in a uniform way, which makes maintaining it somewhat easy. The most difficult part of this pattern was configuring
the executor to supply the right injectors to the commands. Long term, this will make refactoring more difficult as
commands call into one another and state is intertwined with commands.

I expect the Redux pattern to be a little cleaner except thunks will still put multiple dependencies on the shape of
the store in the same way as injectable commands. The transformation of data through reducers will help to decouple
command's dual responsibility. Reducers clearly define where data is changed and actions/action creators is where
stuff is done.

### Containerization

Derpymon already made good use of containers early on in the project so we were able to skip this step

### Command Functions

Changing the injectable commands to command functions where all of the necessary store data is provided as properties to
the command was relatively easy. The biggest challenge occurs when one command would call into other commands. Usually
this would be handled via the executor providing configured dependencies to the new command, but without an executor
coordinating this data, parent commands would need to be passed all of the properties necessary to call any potential
child command function. In larger application this will create a cascading complexity for this step.

### Updating Containers

Updating Containers to use the new command functions was easy. Containers are now injected with the necessary data so
the command functions dependencies are satisfied and the Executor and injectable commands were removed.

### Event handlers

The keyboard listeners were moved into `keyboardMiddleware` in anticipation for managing external keyboard events as
a middleware layer. Two methods: `attachListeners` and `detachListeners` were created that will later be replaced with
attach and detach keyboard events actions.

### Create the initial state

The initial state was created for Derpymon based on the three injectors. The initial state's shape has been described
by an interface and it's been added as a `ReduxInjector` as part of Dojo 2's interop package. We haven't replaced the
original stores yet. Because the commands are responsible for what actions and reducers will be responsible for we'll
want to go back and replace commands and store in sections. Fortunately our data and commands are already well
compartmentalized.

# Derpymon Go

Catch derpy monsters in VR

![derpmander](./docs/derpymon.png)

NOTE: This does not currently work without a modified version of Maquette
NOTE: This does not currently work without a modified version of Maquette (included)

## Quickstart

Expand All @@ -29,6 +102,3 @@ VR a bit before debugging if you experience these issues.
1. Wait. **Do not put your phone in the DayDream headset** Chrome Beta will switch into DayDream mode.
1. Open _Remote Devices_ in Chrome debug tools, connect to your phone, and inspect the Derpymon page.




40 changes: 40 additions & 0 deletions intern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"capabilities+": {
"project": "Derpymon",
"name": "devpaul/derpymon"
},
"browser": {
"plugins": [
"./_build/src/src.js"
],
"suites": [
"./_build/tests/unit/all.js"
]
},
"coverage": false,
"node": {
"reporters": [{ "name": "simple" }]
},
"configs": {
"local": {
"tunnel": "selenium",
"environments": [
{ "browserName": "chrome" }
]
},
"saucelabs": {
"tunnel": "saucelabs",
"capabilities+": {
"fixSessionCapabilities": false
},

"defaultTimeout": 10000,
"environments": [
{ "browserName": "MicrosoftEdge", "version": "15.15063", "platform": "Windows 10" },
{ "browserName": "firefox", "version": "57", "platform": "Windows 10" },
{ "browserName": "chrome", "version": "63", "platform": "Windows 10" }
],
"maxConcurrency": 4
}
}
}
49 changes: 30 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,57 @@
"license": "MPL-2.0",
"scripts": {
"build": "dojo build",
"build-tests": "dojo build -t --disableLazyWidgetDetection",
"ci": "npm run build-tests && npm run saucelabs",
"element": "dojo build --element=src/createDerpymonElement.ts",
"lint": "tslint --project tslint.json",
"saucelabs": "intern config=@saucelabs",
"selenium": "intern config=@local",
"serve": "ts-node ./support/webserv",
"test": "dojo test",
"watch": "dojo build -w"
},
"repository": {
"git": "https://github.com/devpaul/derpymon"
},
"dependencies": {
"@dojo/core": "^0.1.0",
"@dojo/has": "^0.1.0",
"@dojo/i18n": "^0.1.0",
"@dojo/routing": "^0.1.0",
"@dojo/shim": "^0.1.0",
"@dojo/widget-core": "^0.1.0",
"@types/aframe": "^0.5.0",
"@webcomponents/custom-elements": "^1.0.2",
"@dojo/core": "^0.3.1",
"@dojo/has": "^0.1.2",
"@dojo/i18n": "^0.4.1",
"@dojo/interfaces": "^0.2.1",
"@dojo/interop": "^0.4.0",
"@dojo/routing": "^0.4.1",
"@dojo/shim": "^0.2.5",
"@dojo/widget-core": "^0.6.5",
"@types/aframe": "^0.7.0",
"@webcomponents/custom-elements": "^1.0.8",
"@webcomponents/html-imports": "^1.0.1",
"aframe": "^0.7.0",
"aframe-environment-component": "^1.0.0",
"aframe-physics-system": "^2.1.0",
"cannon": "^0.6.2",
"maquette": "devpaul/maquette#custom-elements-dist"
"redux": "^3.7.2",
"redux-thunk": "^2.2.0"
},
"devDependencies": {
"@dojo/cli": "next",
"@dojo/cli-build-webpack": "next",
"@dojo/cli-test-intern": "next",
"@dojo/interfaces": "next",
"@dojo/loader": "next",
"@types/chai": "~3.4.0",
"@dojo/cli": "^0.5.0",
"@dojo/cli-build-webpack": "^0.4.1",
"@dojo/cli-test-intern": "^0.4.0",
"@dojo/test-extras": "^0.4.1",
"@types/glob": "~5.0.0",
"@types/grunt": "~0.4.0",
"@types/ip": "0.0.30",
"@types/node": "^6.0.46",
"@types/opn": "^3.0.28",
"@types/sinon": "^1.16.35",
"chai": "^3.5.0",
"intern": "~3.4.1",
"chai": "^4.1.2",
"intern": "^4.1.4",
"ip": "^1.1.5",
"opn": "^5.1.0",
"sinon": "^2.0.0",
"ts-node": "^3.3.0",
"typescript": "^2.4.2",
"ts-node": "^4.0.2",
"tslint-sitepen": "^0.1.0",
"typescript": "^2.6.2",
"webserv": "^0.11.1"
}
}
6 changes: 6 additions & 0 deletions src/actions/ActionType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const enum ActionType {
AttachKeyboard = 'attachKeyboard',
DetachKeyboard = 'detachKeyboard',
ThrowDerpyball = 'throwDerpyball',
RemoveDerpyball = 'removeDerpyball'
}
15 changes: 15 additions & 0 deletions src/actions/outside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ActionType } from './ActionType';
import { Throw } from '../context/OutsideContext';

export function removeDerpyball() {
return {
type: ActionType.RemoveDerpyball
};
}

export function throwDerpyball(ballThrow: Throw) {
return {
type: ActionType.ThrowDerpyball,
payload: ballThrow
};
}
21 changes: 13 additions & 8 deletions src/commands/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
import Executor, { Action } from '../framework/Executor';
import AppContext, { ApplicationState } from '../context/AppContext';
import { ActionType } from '../initialize';
import registerHeightComponent from '../components/heightComponent';
import AssetContext from '../context/AssetContext';
import OutsideContext from '../context/OutsideContext';
import monsters from '../data/monsters';
import registerMonsters from './registerMonsters';
import randomizeEncounter from './randomizeEncounter';

export type InitializeAction = Action<undefined, [ AppContext, Executor ]>;

export default function initialize({ state: [ app, executor ] }: InitializeAction) {
export default function initialize(app: AppContext, assets: AssetContext, outside: OutsideContext) {
if (app.state !== ApplicationState.Initial) {
throw new Error('Application already initialized');
}

app.isLoadingState = true;

if (!app.initialized.monsters) {
executor.execute(ActionType.LoadMonsters);
// TODO NOTE: This is the problem w/ multi-state w/o an executor to inject
// commands accumulate requirements from their children making it a mess to unwind
registerMonsters(monsters, assets, outside);
app.initialized.monsters = true;
}

if (!app.initialized.aframe) {
registerHeightComponent();
app.initialized.aframe = true;
}

if (app.initialized.monsters && app.initialized.aframe) {
executor.execute(ActionType.RandomizeEncounter);
randomizeEncounter(outside);
app.isLoadingState = false;
app.state = ApplicationState.Outside;
}
};
}
9 changes: 0 additions & 9 deletions src/commands/loadMonsters.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/commands/loadedMonsters.ts

This file was deleted.

7 changes: 2 additions & 5 deletions src/commands/randomizeEncounter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Action } from '../framework/Executor';
import OutsideContext from '../context/OutsideContext';

export type RandomizeEncounterAction = Action<undefined, OutsideContext>;

export default function randomizeEncounter({ state: outside }: RandomizeEncounterAction) {
export default function randomizeEncounter(outside: OutsideContext) {
const monsters = outside.getMonsterDefinitions();
const num = Math.floor(Math.random() * monsters.length);
const monster = monsters[num];
Expand All @@ -19,4 +16,4 @@ export default function randomizeEncounter({ state: outside }: RandomizeEncounte
height,
distance
});
};
}
15 changes: 4 additions & 11 deletions src/commands/registerMonsters.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import { Action } from '../framework/Executor';
import { MonsterConfigurationItem } from '../configuration/monsters';
import { MonsterConfigurationItem } from '../data/monsters';
import OutsideContext from '../context/OutsideContext';
import { throws } from '../util/properties';
import AssetContext from '../context/AssetContext';

export type RegisterMonstersAction = Action<Array<MonsterConfigurationItem>, [ AssetContext, OutsideContext ]>;

export default function registerMonsters({
payload: monsters = throws(),
state: [ appContext, outsideContext ] = throws()
}: RegisterMonstersAction) {
export default function registerMonsters(monsters: MonsterConfigurationItem[], assets: AssetContext, outsideContext: OutsideContext) {
for (let monster of monsters) {
appContext.addObjMtlAssets(monster.name, monster.obj, monster.mtl);
assets.addObjMtlAssets(monster.name, monster.assets.obj, monster.assets.mtl);
outsideContext.addMonster({
name: monster.name,
heights: monster.heights,
environment: monster.environment
});
}
};
}
Loading