diff --git a/spikes/stacks/redux-loop/.babelrc b/spikes/stacks/redux-loop/.babelrc
new file mode 100644
index 00000000..82cc857a
--- /dev/null
+++ b/spikes/stacks/redux-loop/.babelrc
@@ -0,0 +1,15 @@
+{
+ "presets": [
+ "react",
+ "es2015"
+ ],
+ "plugins": [
+ ["transform-object-rest-spread", {
+ "useBuiltIns": true
+ }],
+ "add-module-exports",
+ "transform-es2015-modules-commonjs",
+ "react-hot-loader/babel"
+ ],
+ "sourceMaps": "both"
+}
diff --git a/spikes/stacks/redux-loop/.eslintignore b/spikes/stacks/redux-loop/.eslintignore
new file mode 100644
index 00000000..683e721c
--- /dev/null
+++ b/spikes/stacks/redux-loop/.eslintignore
@@ -0,0 +1,3 @@
+/node_modules
+coverage
+.nyc_output
diff --git a/spikes/stacks/redux-loop/.eslintrc b/spikes/stacks/redux-loop/.eslintrc
new file mode 100644
index 00000000..19bd88dc
--- /dev/null
+++ b/spikes/stacks/redux-loop/.eslintrc
@@ -0,0 +1,29 @@
+{
+ "extends": "semistandard",
+ "parser": "babel-eslint",
+ "parserOptions": {
+ "ecmaVersion": 7,
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "sourceType": "module"
+ },
+ "plugins": [
+ "babel",
+ "react"
+ ],
+ "rules": {
+ "generator-star-spacing": 0,
+ "babel/generator-star-spacing": 1,
+ "space-before-function-paren": [2, "never"],
+ "react/jsx-uses-react": 2,
+ "react/jsx-uses-vars": 2,
+ "react/react-in-jsx-scope": 2,
+ "object-curly-newline": ["error", {
+ "minProperties": 1
+ }],
+ "sort-vars": ["error", {
+ "ignoreCase": true
+ }]
+ }
+}
\ No newline at end of file
diff --git a/spikes/stacks/redux-loop/.gitignore b/spikes/stacks/redux-loop/.gitignore
new file mode 100644
index 00000000..c21b0ba0
--- /dev/null
+++ b/spikes/stacks/redux-loop/.gitignore
@@ -0,0 +1,4 @@
+/node_modules
+coverage
+.nyc_output
+npm-debug.log
diff --git a/spikes/stacks/redux-loop/.storybook/config.js b/spikes/stacks/redux-loop/.storybook/config.js
new file mode 100644
index 00000000..d17d412a
--- /dev/null
+++ b/spikes/stacks/redux-loop/.storybook/config.js
@@ -0,0 +1,5 @@
+const Storybook = require('@kadira/storybook');
+
+Storybook.configure(() => {
+ require('../stories');
+}, module);
diff --git a/spikes/stacks/redux-loop/package.json b/spikes/stacks/redux-loop/package.json
new file mode 100644
index 00000000..c5c852e4
--- /dev/null
+++ b/spikes/stacks/redux-loop/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "redux-loop-spike",
+ "private": true,
+ "license": "private",
+ "main": "src/server/index.js",
+ "scripts": {
+ "start": "node .",
+ "lint": "eslint .",
+ "test": "NODE_ENV=test nyc ava test/*.js --fail-fast --verbose --tap",
+ "open": "nyc report --reporter=html & open coverage/index.html",
+ "coverage": "nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100",
+ "storybook": "start-storybook -p 6006",
+ "build-storybook": "build-storybook"
+ },
+ "dependencies": {
+ "aphrodite": "^0.6.0",
+ "babel-eslint": "^7.0.0",
+ "babel-loader": "^6.2.5",
+ "babel-plugin-add-module-exports": "^0.2.1",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.16.0",
+ "babel-plugin-transform-object-rest-spread": "^6.16.0",
+ "babel-plugin-transform-runtime": "^6.15.0",
+ "babel-preset-es2015": "^6.16.0",
+ "babel-preset-react": "^6.16.0",
+ "babel-preset-react-hmre": "^1.1.1",
+ "babel-runtime": "^6.11.6",
+ "component-emitter": "^1.2.1",
+ "crosstab": "^0.2.12",
+ "express-graphql": "^0.5.4",
+ "graphql": "^0.7.2",
+ "graphql-fetch": "^1.0.0",
+ "json-loader": "^0.5.4",
+ "lodash.find": "^4.6.0",
+ "lodash.get": "^4.4.2",
+ "lodash.values": "^4.3.0",
+ "node-uuid": "^1.4.7",
+ "react": "^15.3.2",
+ "react-dom": "^15.3.2",
+ "react-hot-loader": "^3.0.0-beta.6",
+ "react-intl": "^2.1.5",
+ "react-redux": "^4.4.5",
+ "react-router": "^4.0.0-alpha.4",
+ "reduce-reducers": "^0.1.2",
+ "redux": "^3.6.0",
+ "redux-actions": "^0.12.0",
+ "redux-batched-actions": "^0.1.3",
+ "redux-logger": "^2.7.0",
+ "redux-loop": "^2.2.2",
+ "redux-promise-middleware": "^4.1.0",
+ "reselect": "^2.5.4",
+ "webpack": "^1.13.2",
+ "webpack-dev-server": "^1.16.2"
+ },
+ "devDependencies": {
+ "@kadira/storybook": "^2.24.0",
+ "ava": "^0.16.0",
+ "babel-register": "^6.16.3",
+ "enzyme": "^2.4.1",
+ "eslint": "^3.8.0",
+ "eslint-config-semistandard": "^7.0.0",
+ "eslint-config-standard": "^6.2.0",
+ "eslint-plugin-babel": "^3.3.0",
+ "eslint-plugin-promise": "^3.0.0",
+ "eslint-plugin-react": "^6.4.1",
+ "eslint-plugin-standard": "^2.0.1",
+ "nyc": "^8.3.1",
+ "react-addons-test-utils": "^15.3.2"
+ },
+ "ava": {
+ "require": [
+ "babel-register"
+ ],
+ "babel": "inherit"
+ }
+}
diff --git a/spikes/stacks/redux-loop/src/client/api.js b/spikes/stacks/redux-loop/src/client/api.js
new file mode 100644
index 00000000..6f8e6afc
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/api.js
@@ -0,0 +1,49 @@
+var fetch = require('graphql-fetch')(`${document.location.origin}/graphql`);
+
+exports.fetchChanges = () => {
+ return fetch(`
+ query {
+ changes {
+ id,
+ product {
+ id,
+ artist,
+ title,
+ label,
+ format,
+ price,
+ currency
+ },
+ price,
+ currency
+ }
+ }
+ `).then(({
+ data
+ }) => {
+ return data.changes;
+ });
+};
+
+exports.removeChange = (id) => {
+ console.log(`
+ mutation {
+ removeChange(id: "${id}")
+ }
+`);
+ return fetch(`
+ mutation {
+ removeChange(id: "${id}") {
+ id
+ }
+ }
+ `).then(({
+ errors
+ }) => {
+ if (!errors) {
+ return;
+ }
+
+ throw new Error(errors[0].message);
+ });
+};
diff --git a/spikes/stacks/redux-loop/src/client/components/change.js b/spikes/stacks/redux-loop/src/client/components/change.js
new file mode 100644
index 00000000..3a317d26
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/components/change.js
@@ -0,0 +1,14 @@
+const React = require('react');
+
+module.exports = ({
+ price,
+ currency,
+ product,
+ id
+}) => {
+ return (
+
+ {product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price}
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/components/changes.js b/spikes/stacks/redux-loop/src/client/components/changes.js
new file mode 100644
index 00000000..31bd1fa1
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/components/changes.js
@@ -0,0 +1,32 @@
+const ReactRouter = require('react-router');
+const React = require('react');
+
+const {
+ Link
+} = ReactRouter;
+
+module.exports = ({
+ changes = [],
+ pathname
+}) => {
+ const lis = changes.map(({
+ price,
+ currency,
+ product,
+ id
+ }) => {
+ return (
+
+
+ {product.artist}: {product.title} - {product.currencr}{product.price} > {currency}{price}
+
+
+ );
+ });
+
+ return (
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/components/loader.js b/spikes/stacks/redux-loop/src/client/components/loader.js
new file mode 100644
index 00000000..f9ec9b3f
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/components/loader.js
@@ -0,0 +1,51 @@
+const React = require('react');
+const Loader = require('react-loader');
+
+module.exports = React.createClass({
+ fetch: function() {
+ const {
+ fetch,
+ loading,
+ loaded
+ } = this.props;
+
+ if (fetch && !loading && !loaded) {
+ fetch();
+ }
+ },
+ componentDidMount: function() {
+ this.fetch();
+ },
+ componentDidUpdate: function(nextProps) {
+ const updated = (
+ nextProps.loaded !== this.props.loaded &&
+ nextProps.loading !== this.props.loading
+ );
+
+ if (!updated) {
+ return;
+ }
+
+ this.fetch();
+ },
+ render: function() {
+ const {
+ loading,
+ loaded,
+ render,
+ children
+ } = this.props;
+
+ const _loaded = !loading && !loaded;
+ const component = _loaded ? (children || render()) : null;
+
+ return (
+
+ {component}
+
+ );
+ }
+});
diff --git a/spikes/stacks/redux-loop/src/client/components/printers.js b/spikes/stacks/redux-loop/src/client/components/printers.js
new file mode 100644
index 00000000..c5e71317
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/components/printers.js
@@ -0,0 +1,39 @@
+const React = require('react');
+
+module.exports = ({
+ printers = [],
+ locked = '',
+ onClick
+}) => {
+ const _onClick = (id) => {
+ return () => {
+ onClick(id);
+ };
+ };
+
+ const lis = printers.map(({
+ name,
+ lock,
+ id
+ }) => {
+ const msg = (() => {
+ if (!lock) {
+ return '';
+ }
+
+ return (locked === id) ? '(Locked to you)' : `(Locked to ${lock})`;
+ })();
+
+ return (
+
+ {name} {msg}
+
+ );
+ });
+
+ return (
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/containers/app.js b/spikes/stacks/redux-loop/src/client/containers/app.js
new file mode 100644
index 00000000..ab7463fe
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/containers/app.js
@@ -0,0 +1,55 @@
+const ReactRedux = require('react-redux');
+const ReactRouter = require('react-router');
+const React = require('react');
+
+const NotFound = require('./not-found');
+const Home = require('./home');
+const Print = require('./print');
+
+const actions = require('../state/actions');
+
+const {
+ connect
+} = ReactRedux;
+
+const {
+ Miss,
+ Match
+} = ReactRouter;
+
+const {
+ updateRouter
+} = actions;
+
+const App = connect()(React.createClass({
+ componentDidMount: function() {
+ require('../worker').on('action', this.props.dispatch);
+ },
+ render: function() {
+ const {
+ children,
+ router,
+ dispatch
+ } = this.props;
+
+ // ugly hack needed because of a limitation of react-router api
+ // that doens't pass it's instance to matched routes
+ dispatch(updateRouter(router));
+
+ return (
+
+ {children}
+
+ );
+ }
+}));
+
+module.exports = (props) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/containers/home.js b/spikes/stacks/redux-loop/src/client/containers/home.js
new file mode 100644
index 00000000..952a726a
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/containers/home.js
@@ -0,0 +1,14 @@
+const ReactRouter = require('react-router');
+const React = require('react');
+
+const {
+ Link
+} = ReactRouter;
+
+module.exports = () => {
+ return (
+
+ Start
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/containers/not-found.js b/spikes/stacks/redux-loop/src/client/containers/not-found.js
new file mode 100644
index 00000000..ed7e90b8
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/containers/not-found.js
@@ -0,0 +1,7 @@
+const React = require('react');
+
+module.exports = () => {
+ return (
+ Not found
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/containers/print.js b/spikes/stacks/redux-loop/src/client/containers/print.js
new file mode 100644
index 00000000..93730f49
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/containers/print.js
@@ -0,0 +1,136 @@
+const ReactRouter = require('react-router');
+const ReactRedux = require('react-redux');
+const React = require('react');
+const find = require('lodash.find');
+
+const Loader = require('../components/loader');
+const Printers = require('../components/printers');
+const Changes = require('../components/changes');
+const Change = require('../components/change');
+
+const actions = require('../state/actions');
+
+const {
+ fetchChanges,
+ lockPrinter,
+ print,
+ transitionTo
+} = actions;
+
+const {
+ Match
+} = ReactRouter;
+
+const {
+ connect
+} = ReactRedux;
+
+const Print = ({
+ pathname,
+ printers = [],
+ changes = [],
+ lockPrinter,
+ fetchChanges,
+ onPrint,
+ loaded,
+ loading,
+ locked,
+ router
+}) => {
+ const allChanges = () => {
+ return (
+
+ );
+ };
+
+ const singleChange = ({
+ params
+ }) => {
+ const change = find(changes, (change) => {
+ return change.id === params.id;
+ });
+
+ if (!change) {
+ return (
+ Change not found
+ );
+ }
+
+ const _onPrint = (id) => {
+ return () => {
+ return onPrint(id);
+ };
+ };
+
+ // TODO: don't load all changes
+ return (
+
+ );
+ };
+
+ return (
+
+ );
+};
+
+const mapStateToProps = (state) => {
+ return {
+ loaded: state.ui.changes.loaded,
+ loading: state.ui.changes.loading,
+ changes: state.data.changes,
+ printers: state.data.printers,
+ locked: state.ui.printers.locked
+ };
+};
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ lockPrinter: (id) => {
+ return dispatch(lockPrinter(id));
+ },
+ fetchChanges: () => {
+ return dispatch(fetchChanges());
+ },
+ onPrint: (id) => {
+ return dispatch(print(id)).then(() => {
+ return dispatch(transitionTo('/print'));
+ });
+ }
+ };
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Print);
diff --git a/spikes/stacks/redux-loop/src/client/index.js b/spikes/stacks/redux-loop/src/client/index.js
new file mode 100644
index 00000000..10c5a842
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/index.js
@@ -0,0 +1,19 @@
+require('./worker'); // singleton
+
+const React = require('react');
+const ReactDOM = require('react-dom');
+
+const render = () => {
+ const Root = require('./root');
+
+ ReactDOM.render(
+ ,
+ document.getElementById('root')
+ );
+};
+
+render();
+
+if (module.hot) {
+ module.hot.accept('./root', render);
+}
diff --git a/spikes/stacks/redux-loop/src/client/intl.json b/spikes/stacks/redux-loop/src/client/intl.json
new file mode 100644
index 00000000..077404aa
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/intl.json
@@ -0,0 +1,3 @@
+{
+
+}
\ No newline at end of file
diff --git a/spikes/stacks/redux-loop/src/client/root.js b/spikes/stacks/redux-loop/src/client/root.js
new file mode 100644
index 00000000..4ecda442
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/root.js
@@ -0,0 +1,46 @@
+const React = require('react');
+const ReactHotLoader = require('react-hot-loader');
+const ReactRouter = require('react-router');
+const ReactRedux = require('react-redux');
+const ReactIntl = require('react-intl');
+
+const App = require('./containers/app');
+const store = require('./state/store');
+
+const {
+ AppContainer
+} = ReactHotLoader;
+
+const {
+ BrowserRouter
+} = ReactRouter;
+
+const {
+ Provider
+} = ReactRedux;
+
+const {
+ IntlProvider
+} = ReactIntl;
+
+// http://stackoverflow.com/a/38150585
+const locale = (
+ navigator.languages && navigator.languages[0] || // Chrome / Firefox
+ navigator.language || // All browsers
+ navigator.userLanguage // IE <= 10
+);
+
+module.exports = () => {
+ return (
+
+
+
+ {App}
+
+
+
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/actions/app.js b/spikes/stacks/redux-loop/src/client/state/actions/app.js
new file mode 100644
index 00000000..65b4371e
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/actions/app.js
@@ -0,0 +1,25 @@
+const ReduxActions = require('redux-actions');
+const app = require('../../../../package.json').name;
+const selectors = require('../selectors');
+
+const {
+ router
+} = selectors;
+
+const {
+ createAction
+} = ReduxActions;
+
+const UPDATE_ROUTER = `${app}/changes/UPDATE_ROUTER`;
+
+const updateRouter = createAction(UPDATE_ROUTER);
+
+const transitionTo = (pathname) => (dispatch, getState) => {
+ return router(getState()).transitionTo(pathname);
+};
+
+module.exports = {
+ UPDATE_ROUTER,
+ updateRouter,
+ transitionTo
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/actions/changes.js b/spikes/stacks/redux-loop/src/client/state/actions/changes.js
new file mode 100644
index 00000000..389e4249
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/actions/changes.js
@@ -0,0 +1,28 @@
+const app = require('../../../../package.json').name;
+const api = require('../../api');
+
+const FETCH_CHANGES = `${app}/changes/FETCH_CHANGES`;
+const REMOVE_CHANGE = `${app}/changes/REMOVE_CHANGE`;
+
+const fetchChanges = () => {
+ return {
+ type: FETCH_CHANGES,
+ payload: api.fetchChanges()
+ };
+};
+
+const removeChange = (id) => (dispatch) => {
+ return dispatch({
+ type: REMOVE_CHANGE,
+ payload: api.removeChange(id)
+ }).then(() => {
+ return dispatch(fetchChanges());
+ });
+};
+
+module.exports = {
+ FETCH_CHANGES,
+ REMOVE_CHANGE,
+ fetchChanges,
+ removeChange
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/actions/index.js b/spikes/stacks/redux-loop/src/client/state/actions/index.js
new file mode 100644
index 00000000..997b6edd
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/actions/index.js
@@ -0,0 +1,5 @@
+module.exports = {
+ ...require('./app'),
+ ...require('./printers'),
+ ...require('./changes')
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/actions/printers.js b/spikes/stacks/redux-loop/src/client/state/actions/printers.js
new file mode 100644
index 00000000..6bbde8df
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/actions/printers.js
@@ -0,0 +1,159 @@
+const ReduxActions = require('redux-actions');
+const app = require('../../../../package.json').name;
+const find = require('lodash.find');
+const changes = require('./changes');
+
+const {
+ createAction
+} = ReduxActions;
+
+const {
+ removeChange
+} = changes;
+
+const UPDATE_PRINTERS = `${app}/printers/UPDATE_PRINTERS`;
+const UPDATE_WORKER_ID = `${app}/printers/UPDATE_WORKER_ID`;
+const EMIT_LOCK_PRINTER = `${app}/printers/EMIT_LOCK_PRINTER`;
+const LOCK_PRINTER = `${app}/printers/LOCK_PRINTER`;
+const PRINTER_NOT_FOUND = `${app}/printers/PRINTER_NOT_FOUND`;
+const DO_PRINT = `${app}/printers/DO_PRINT`;
+const EMIT_PRINT_JOB = `${app}/printers/EMIT_PRINT_JOB`;
+const CONFIRM_OVERRIDE = `${app}/printers/CONFIRM_OVERRIDE`;
+const CONFIRM_LOCK = `${app}/printers/CONFIRM_LOCK`;
+const PROMPT_LOCK = `${app}/printers/PROMPT_LOCK`;
+const PROMPT_CONFIRM_LOCK = `${app}/printers/PROMPT_CONFIRM_LOCK`;
+const PRINT = `${app}/printers/PRINT`;
+
+// confirm should be an async op,
+// let's mock it that way
+const confirm = (msg) => {
+ return new Promise((resolve, reject) => {
+ resolve(window.confirm(msg));
+ });
+};
+
+// prompt should be an async op,
+// let's mock it that way
+const prompt = (msg) => {
+ return new Promise((resolve, reject) => {
+ resolve(window.prompt(msg));
+ });
+};
+
+// alert should be an async op,
+// let's mock it that way
+const alert = (msg) => {
+ return new Promise((resolve, reject) => {
+ resolve(window.alert(msg));
+ });
+};
+
+const confirmOverride = createAction(CONFIRM_OVERRIDE);
+const confirmLock = createAction(CONFIRM_LOCK);
+const promptLock = createAction(PROMPT_LOCK);
+const updatePrinters = createAction(UPDATE_PRINTERS);
+const updateWorkerId = createAction(UPDATE_WORKER_ID);
+const promptOrConfirmLock = createAction(PROMPT_CONFIRM_LOCK);
+const lockPrinter = createAction(LOCK_PRINTER);
+const printerNotFound = createAction(PRINTER_NOT_FOUND);
+const print = createAction(PRINT);
+
+// const print = (changeId) => (dispatch, getState) => {
+// const {
+// ui,
+// data
+// } = getState();
+//
+// const {
+// printers
+// } = data;
+//
+// const {
+// printers: {
+// locked
+// }
+// } = ui;
+//
+// const worker = require('../../worker');
+//
+// const _print = () => {
+// return dispatch({
+// type: EMIT_PRINT_JOB,
+// payload: worker.dispatch({
+// type: 'PRINT',
+// payload: changeId
+// })
+// }).then(() => {
+// return dispatch(removeChange(changeId));
+// });
+// };
+//
+// const lock = (printerId) => {
+// return dispatch({
+// type: EMIT_LOCK_PRINTER,
+// payload: worker.dispatch({
+// type: 'LOCK_PRINTER',
+// payload: printerId
+// })
+// }).then(_print);
+// };
+//
+// const askToOverride = (printerId) => {
+// const msg = `Printer ${printerId} already locked! Do you want to override?`;
+// return confirm(msg).then((yes) => {
+// return yes ? lock(printerId) : null;
+// });
+// };
+//
+// const askToLock = () => {
+// const msg = `Please select a printer to lock: ${
+// printers.map(({
+// id,
+// name
+// }) => {
+// return `\n(${id}) ${name}`;
+// })
+// }`;
+//
+// return prompt(msg).then((printerId) => {
+// const printer = find(printers, ['id', printerId]);
+//
+// if (!printer) {
+// return alert(`Printer ${printerId} not found. Try again`).then(() => {
+// return print(printerId)(dispatch, getState);
+// });
+// }
+//
+// return printer.lock ? askToOverride(printerId) : lock(printerId);
+// });
+// };
+//
+// return !locked ? askToLock() : _print();
+// };
+
+module.exports = {
+ UPDATE_PRINTERS,
+ UPDATE_WORKER_ID,
+ EMIT_LOCK_PRINTER,
+ LOCK_PRINTER,
+ PRINTER_NOT_FOUND,
+ DO_PRINT,
+ EMIT_PRINT_JOB,
+ CONFIRM_OVERRIDE,
+ CONFIRM_LOCK,
+ PROMPT_LOCK,
+ PROMPT_CONFIRM_LOCK,
+ PRINT,
+ PRINTER_NOT_FOUND,
+ confirm,
+ prompt,
+ alert,
+ updatePrinters,
+ updateWorkerId,
+ confirmOverride,
+ confirmLock,
+ promptLock,
+ promptOrConfirmLock,
+ lockPrinter,
+ print
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/loop-promise.js b/spikes/stacks/redux-loop/src/client/state/loop-promise.js
new file mode 100644
index 00000000..4be218e4
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/loop-promise.js
@@ -0,0 +1,39 @@
+const ReduxActions = require('redux-actions');
+const ReduxLoop = require('redux-loop');
+
+const {
+ Effects
+} = ReduxLoop;
+
+const {
+ createAction
+} = ReduxActions;
+
+module.exports = ({
+ type,
+ payload,
+ ctx
+}) => {
+ return Effects.batch([
+ Effects.constant({
+ type: `${type}_PENDING`
+ }),
+ Effects.promise(() => {
+ return payload
+ .then((res) => {
+ return {
+ type: `${type}_FULFILLED`,
+ payload: res,
+ ctx
+ };
+ })
+ .catch((err) => {
+ return {
+ type: `${type}_REJECTED`,
+ payload: err,
+ ctx
+ };
+ });
+ })
+ ]);
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js b/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js
new file mode 100644
index 00000000..bf5e143c
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/reduce-reducers.js
@@ -0,0 +1,44 @@
+const ReduxLoop = require('redux-loop');
+
+const {
+ loop,
+ isLoop,
+ getEffect,
+ getModel,
+ Effects
+} = ReduxLoop;
+
+const optimizeBatch = (effects) => {
+ switch(effects.length) {
+ case 0:
+ return Effects.none();
+ case 1:
+ return effects[0];
+ default:
+ return Effects.batch(effects);
+ }
+};
+
+module.exports = (...reducers) => {
+ return (state, action) => {
+ const effects = [];
+
+ const next = reducers.reduce((sum, reducer) => {
+ let state = reducer(sum, action);
+
+ if (!isLoop(state)) {
+ return state;
+ }
+
+ effects.push(getEffect(state));
+ state = getModel(state);
+
+ return state;
+ }, state);
+
+ return loop(
+ next,
+ optimizeBatch(effects)
+ );
+ };
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/app.js b/spikes/stacks/redux-loop/src/client/state/reducers/app.js
new file mode 100644
index 00000000..fc47f8e8
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/reducers/app.js
@@ -0,0 +1,19 @@
+const ReduxActions = require('redux-actions');
+const actions = require('../actions');
+
+const {
+ handleActions
+} = ReduxActions;
+
+const {
+ UPDATE_ROUTER
+} = actions;
+
+exports.ui = handleActions({
+ [UPDATE_ROUTER]: (state, action) => {
+ return {
+ ...state,
+ router: action.payload
+ };
+ }
+}, {});
diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/changes.js b/spikes/stacks/redux-loop/src/client/state/reducers/changes.js
new file mode 100644
index 00000000..7f5155c6
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/reducers/changes.js
@@ -0,0 +1,43 @@
+const ReduxActions = require('redux-actions');
+const actions = require('../actions');
+
+const {
+ handleActions
+} = ReduxActions;
+
+const {
+ FETCH_CHANGES
+} = actions;
+
+exports.data = handleActions({
+ [`${FETCH_CHANGES}_FULFILLED`]: (state, action) => {
+ return action.payload;
+ }
+}, []);
+
+exports.ui = handleActions({
+ [`${FETCH_CHANGES}_PENDING`]: (state, action) => {
+ return {
+ ...state,
+ loading: true
+ };
+ },
+ [`${FETCH_CHANGES}_FULFILLED`]: (state, action) => {
+ return {
+ ...state,
+ loading: false,
+ loaded: false
+ };
+ },
+ [`${FETCH_CHANGES}_REJECTED`]: (state, action) => {
+ // TODO: deal with error
+ return {
+ ...state,
+ loading: false,
+ loaded: false
+ };
+ }
+}, {
+ loading: false,
+ loaded: false
+});
diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/index.js b/spikes/stacks/redux-loop/src/client/state/reducers/index.js
new file mode 100644
index 00000000..d21d14fd
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/reducers/index.js
@@ -0,0 +1,27 @@
+const reduceReducers = require('../reduce-reducers');
+const ReduxLoop = require('redux-loop');
+
+const app = require('./app');
+const printers = require('./printers');
+const changes = require('./changes');
+
+const {
+ combineReducers
+} = ReduxLoop;
+
+module.exports = () => reduceReducers(
+ // app.global,
+ printers.global,
+ // changes.global
+ combineReducers({
+ data: combineReducers({
+ printers: printers.data,
+ changes: changes.data
+ }),
+ ui: combineReducers({
+ changes: changes.ui,
+ printers: printers.ui,
+ app: app.ui
+ })
+ })
+);
diff --git a/spikes/stacks/redux-loop/src/client/state/reducers/printers.js b/spikes/stacks/redux-loop/src/client/state/reducers/printers.js
new file mode 100644
index 00000000..a0de895f
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/reducers/printers.js
@@ -0,0 +1,238 @@
+const loopPromise = require('../loop-promise');
+const ReduxActions = require('redux-actions');
+const ReduxLoop = require('redux-loop');
+const find = require('lodash.find');
+const actions = require('../actions');
+
+const {
+ handleActions
+} = ReduxActions;
+
+const {
+ loop,
+ Effects
+} = ReduxLoop;
+
+const {
+ UPDATE_WORKER_ID,
+ UPDATE_PRINTERS,
+ LOCK_PRINTER,
+ PROMPT_CONFIRM_LOCK,
+ CONFIRM_LOCK,
+ PROMPT_LOCK,
+ CONFIRM_OVERRIDE,
+ EMIT_LOCK_PRINTER,
+ PRINTER_NOT_FOUND,
+ printerNotFound,
+ confirmOverride,
+ promptOrConfirmLock,
+ confirmLock,
+ promptLock,
+ confirm,
+ alert,
+ prompt
+} = actions;
+
+exports.data = handleActions({
+ [UPDATE_PRINTERS]: (state, action) => {
+ return action.payload;
+ }
+}, []);
+
+exports.ui = handleActions({
+ [UPDATE_WORKER_ID]: (state, action) => {
+ return {
+ ...state,
+ id: action.payload
+ };
+ },
+ [UPDATE_PRINTERS]: (state, action) => {
+ const locked = (find(action.payload, (printer) => {
+ return (
+ printer.lock &&
+ printer.lock === state.id
+ );
+ }) || {}).id || '';
+
+ return {
+ ...state,
+ locked
+ };
+ }
+}, {
+ id: '',
+ locked: ''
+});
+
+exports.global = handleActions({
+ [LOCK_PRINTER]: (state, {
+ payload
+ }) => {
+ const {
+ ui,
+ data
+ } = state;
+
+ const {
+ printers
+ } = data;
+
+ const {
+ printers: {
+ locked
+ }
+ } = ui;
+
+ if (locked === payload) {
+ return state;
+ }
+
+ const printer = find(printers, ['id', payload]);
+
+ if (!printer) {
+ return loop(
+ state,
+ Effects.constant(printerNotFound(payload))
+ );
+ }
+
+ const action = (printer.lock ? confirmOverride : promptOrConfirmLock)(payload);
+
+ return loop(
+ state,
+ Effects.constant(action)
+ );
+ },
+ [PROMPT_CONFIRM_LOCK]: (state, {
+ payload
+ }) => {
+ const action = (payload ? confirmLock : promptLock)(payload);
+
+ return loop(
+ state,
+ Effects.constant(action)
+ );
+ },
+ [CONFIRM_LOCK]: (state, {
+ payload
+ }) => {
+ const msg = `Do you want to lock printer ${payload}?`;
+
+ return loop(
+ state,
+ loopPromise({
+ type: CONFIRM_LOCK,
+ payload: confirm(msg),
+ ctx: payload
+ })
+ );
+ },
+ [`${CONFIRM_LOCK}_FULFILLED`]: (state, {
+ payload,
+ ctx
+ }) => {
+ const worker = require('../../worker');
+
+ return !payload ? state : loop(
+ state,
+ loopPromise({
+ type: EMIT_LOCK_PRINTER,
+ payload: worker.dispatch({
+ type: 'LOCK_PRINTER',
+ payload: ctx
+ })
+ })
+ );
+ },
+ [PROMPT_LOCK]: (state, action) => {
+ const {
+ ui,
+ data
+ } = state;
+
+ const {
+ printers
+ } = data;
+
+ const msg = `Please select a printer to lock: ${
+ printers.map(({
+ id,
+ name
+ }) => {
+ return `\n(${id}) ${name}`;
+ })
+ }`;
+
+ return loop(
+ state,
+ loopPromise({
+ type: PROMPT_LOCK,
+ payload: prompt(msg)
+ })
+ );
+ },
+ [`${PROMPT_LOCK}_FULFILLED`]: (state, {
+ payload
+ }) => {
+ const {
+ data: {
+ printers
+ }
+ } = state;
+
+ const printer = find(printers, ['id', payload]);
+
+ if (!printer) {
+ return loop(
+ state,
+ Effects.constant(printerNotFound(payload))
+ );
+ }
+
+ const worker = require('../../worker');
+
+ return loop(
+ state,
+ loopPromise({
+ type: EMIT_LOCK_PRINTER,
+ payload: worker.dispatch({
+ type: 'LOCK_PRINTER',
+ payload: ctx
+ })
+ })
+ );
+ },
+ [CONFIRM_OVERRIDE]: (state, {
+ payload
+ }) => {
+ const msg = `Printer ${payload} already locked! Do you want to override?`;
+
+ return loop(
+ state,
+ loopPromise({
+ type: CONFIRM_LOCK,
+ payload: confirm(msg),
+ ctx: payload
+ })
+ );
+ },
+ [PRINTER_NOT_FOUND]: (state, action) => {
+ const msg = `Printer ${printerId} not found. Try again`;
+
+ return loop(
+ state,
+ loopPromise({
+ type: PRINTER_NOT_FOUND,
+ payload: alert(msg)
+ })
+ );
+ },
+ [`${PRINTER_NOT_FOUND}_FULFILLED`]: (state, action) => {
+ return loop(
+ state,
+ Effects.constant({
+ type: PROMPT_LOCK
+ })
+ );
+ }
+}, {});
diff --git a/spikes/stacks/redux-loop/src/client/state/selectors.js b/spikes/stacks/redux-loop/src/client/state/selectors.js
new file mode 100644
index 00000000..cac1592c
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/selectors.js
@@ -0,0 +1,10 @@
+// const Reselect = require('reselect');
+const get = require('lodash.get');
+
+const router = (state) => {
+ return get(state, 'ui.app.router');
+};
+
+module.exports = {
+ router
+};
diff --git a/spikes/stacks/redux-loop/src/client/state/store.js b/spikes/stacks/redux-loop/src/client/state/store.js
new file mode 100644
index 00000000..b2ccd018
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/state/store.js
@@ -0,0 +1,30 @@
+const createReducer = require('./reducers');
+const enableBatching = require('redux-batched-actions').enableBatching;
+const ReduxLoop = require('redux-loop');
+const promiseMiddleware = require('redux-promise-middleware').default;
+const createLogger = require('redux-logger');
+const redux = require('redux');
+
+const {
+ install
+} = ReduxLoop;
+
+const {
+ createStore,
+ compose,
+ applyMiddleware
+} = redux;
+
+module.exports = (initialState = Object.freeze({})) => {
+ return createStore(
+ enableBatching(createReducer()),
+ initialState,
+ compose(
+ applyMiddleware(
+ // createLogger(),
+ promiseMiddleware()
+ ),
+ install()
+ )
+ );
+};
diff --git a/spikes/stacks/redux-loop/src/client/worker.js b/spikes/stacks/redux-loop/src/client/worker.js
new file mode 100644
index 00000000..2bd5d47e
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/client/worker.js
@@ -0,0 +1,144 @@
+const uuid = require('node-uuid');
+const Emitter = require('component-emitter');
+const crosstab = require('crosstab');
+const values = require('lodash.values');
+const actions = require('./state/actions');
+
+const emitter = module.exports = new Emitter();
+let isMaster = crosstab.util.tabs['MASTER_TAB'].id === crosstab.id;
+const bridge = new Map();
+
+const {
+ updatePrinters,
+ updateWorkerId
+} = actions;
+
+const printers = {
+ '1': {
+ id: '1',
+ name: 'Main printer',
+ lock: ''
+ },
+ '2': {
+ id: '2',
+ name: 'Handled printer',
+ lock: ''
+ }
+};
+
+const handlers = {
+ 'PRINT': (action, fn) => {
+ fn();
+ },
+ 'LOCK_PRINTER': (action, fn) => {
+ const alreadyLocked = values(printers).filter((printer) => {
+ return printer.lock === action._origin;
+ });
+
+ alreadyLocked.forEach((printer) => {
+ printers[printer.id] = {
+ ...printers[printer.id],
+ lock: ''
+ };
+ });
+
+ printers[action.payload] = {
+ ...printers[action.payload],
+ lock: action._origin
+ };
+
+ fn();
+ }
+};
+
+crosstab.util.events.on('message', ({
+ data,
+ origin
+}) => {
+ if (origin === crosstab.id) {
+ return;
+ }
+
+ if (!data || !data.type) {
+ return;
+ }
+
+ if (!data._id) {
+ return emitter.emit('action', data);
+ }
+
+ const b = bridge.get(data._id);
+
+ if (b) {
+ data.error ? b.reject(new Error(data.error)) : b.resolve(data.payload);
+ return bridge.delete(data._id);
+ }
+
+ if (!handlers[data.type]) {
+ return emitter.emit('action', data);
+ }
+
+ handlers[data.type]({
+ ...data,
+ _origin: origin
+ }, (err, res) => {
+ crosstab.broadcast('message', {
+ ...data,
+ error: err && err.message
+ }, origin);
+ });
+});
+
+crosstab.util.events.on(crosstab.util.eventTypes.becomeMaster, () => {
+ isMaster = true;
+});
+
+crosstab.util.events.on(crosstab.util.eventTypes.demoteFromMaster, () => {
+ isMaster = false;
+});
+
+const dispatch = module.exports.dispatch = (action, tab) => {
+ if (isMaster && !tab) {
+ if (handlers[action.type]) {
+ return new Promise(function(resolve, reject) {
+ handlers[action.type]({
+ ...action,
+ _origin: crosstab.id
+ }, function(err, res) {
+ return err ? reject(err) : resolve(res);
+ });
+ });
+ }
+ }
+
+ const id = uuid.v4();
+
+ const then = new Promise((resolve, reject) => {
+ bridge.set(id, {
+ resolve,
+ reject
+ });
+ });
+
+ crosstab.broadcast('message', {
+ ...action,
+ _id: id
+ }, tab);
+
+ return then;
+};
+
+setTimeout(function() {
+ emitter.emit('action', updateWorkerId(crosstab.id));
+}, 450);
+
+setInterval(() => {
+ if (!isMaster) {
+ return;
+ }
+
+ const action = updatePrinters(values(printers));
+
+ emitter.emit('action', action);
+ dispatch(action);
+}, 1000);
diff --git a/spikes/stacks/redux-loop/src/config/webpack.config.js b/spikes/stacks/redux-loop/src/config/webpack.config.js
new file mode 100644
index 00000000..98343222
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/config/webpack.config.js
@@ -0,0 +1,41 @@
+const webpack = require('webpack');
+const path = require('path');
+
+module.exports = {
+ debug: true,
+ devtool: 'eval',
+ context: path.join(__dirname, '../client'),
+ entry: [
+ 'webpack-dev-server/client?http://localhost:3000',
+ 'webpack/hot/only-dev-server',
+ 'react-hot-loader/patch',
+ './index.js'
+ ],
+ output: {
+ path: path.join(__dirname, '../../static'),
+ publicPath: '/static/',
+ filename: 'bundle.js'
+ },
+ plugins: [
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NoErrorsPlugin()
+ ],
+ module: {
+ loaders: [{
+ test: /js?$/,
+ exclude: /node_modules/,
+ include: [
+ path.join(__dirname, '../client')
+ ],
+ loaders: ['babel']
+ }, {
+ test: /\.json?$/,
+ exclude: /node_modules/,
+ include: [
+ path.join(__dirname, '../client'),
+ path.join(__dirname, '../../') // for package.json
+ ],
+ loaders: ['json']
+ }]
+ }
+};
diff --git a/spikes/stacks/redux-loop/src/server/data/changes.json b/spikes/stacks/redux-loop/src/server/data/changes.json
new file mode 100644
index 00000000..fb3f42b7
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/server/data/changes.json
@@ -0,0 +1,57 @@
+{
+ "1": {
+ "id": "1",
+ "format": "LP",
+ "product": "2267150",
+ "price": 19,
+ "currency": "€"
+ },
+ "2": {
+ "id": "2",
+ "product": "5347347",
+ "price": 16,
+ "currency": "€"
+ },
+ "3": {
+ "id": "3",
+ "product": "2485033",
+ "price": 20,
+ "currency": "€"
+ },
+ "3": {
+ "id": "3",
+ "product": "4389520",
+ "price": 17,
+ "currency": "€"
+ },
+ "4": {
+ "id": "4",
+ "product": "2942482",
+ "price": 15,
+ "currency": "€"
+ },
+ "5": {
+ "id": "5",
+ "product": "6549215",
+ "price": 12,
+ "currency": "€"
+ },
+ "6": {
+ "id": "6",
+ "product": "4192697",
+ "price": 8,
+ "currency": "€"
+ },
+ "7": {
+ "id": "7",
+ "product": "1248296",
+ "price": 27,
+ "currency": "€"
+ },
+ "8": {
+ "id": "8",
+ "product": "1174296",
+ "price": 17,
+ "currency": "€"
+ }
+}
diff --git a/spikes/stacks/redux-loop/src/server/data/products.json b/spikes/stacks/redux-loop/src/server/data/products.json
new file mode 100644
index 00000000..a683d718
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/server/data/products.json
@@ -0,0 +1,83 @@
+{
+ "2267150": {
+ "artist": "The National",
+ "title": "High Violet",
+ "label": "4AD, 4AD",
+ "format": "LP",
+ "id": "2267150",
+ "price": 24,
+ "currency": "€"
+ },
+ "5347347": {
+ "artist": "Sensible Soccers",
+ "title": "8",
+ "label": "Groovement Organic Series",
+ "format": "LP",
+ "id": "5347347",
+ "price": 25,
+ "currency": "€"
+ },
+ "2485033": {
+ "artist": "Eddie Vedder",
+ "title": "Into The Wild",
+ "label": "Music On Vinyl",
+ "format": "LP",
+ "id": "2485033",
+ "price": 33,
+ "currency": "€"
+ },
+ "4389520": {
+ "artist": "Daughter (2)",
+ "title": "If You Leave",
+ "label": "4AD",
+ "format": "LP",
+ "id": "4389520",
+ "price": 22,
+ "currency": "€"
+ },
+ "2942482": {
+ "artist": "Bon Iver",
+ "title": "Bon Iver, Bon Iver",
+ "label": "4AD, Jagjaguwar",
+ "format": "LP",
+ "id": "2942482",
+ "price": 29,
+ "currency": "€"
+ },
+ "6549215": {
+ "artist": "Sufjan Stevens",
+ "title": "Carrie & Lowell",
+ "label": "Asthmatic Kitty Records",
+ "format": "LP",
+ "id": "6549215",
+ "price": 20,
+ "currency": "€"
+ },
+ "4192697": {
+ "artist": "Uzi & Ari",
+ "title": "It Is Freezing Out",
+ "label": "Own Records",
+ "format": "LP",
+ "id": "4192697",
+ "price": 17,
+ "currency": "€"
+ },
+ "1248296": {
+ "artist": "Various",
+ "title": "Juno (Music From The Motion Picture)",
+ "label": "Rhino Records (2), Fox Music, Fox Searchlight Pictures",
+ "format": "LP",
+ "id": "1248296",
+ "price": 36,
+ "currency": "€"
+ },
+ "1174296": {
+ "artist": "Radiohead",
+ "title": "In Rainbows",
+ "label": "XL Recordings",
+ "format": "LP",
+ "id": "1174296",
+ "price": 19,
+ "currency": "€"
+ }
+}
diff --git a/spikes/stacks/redux-loop/src/server/index.js b/spikes/stacks/redux-loop/src/server/index.js
new file mode 100644
index 00000000..a503007e
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/server/index.js
@@ -0,0 +1,28 @@
+const webpack = require('webpack');
+const webpackConfig = require('../config/webpack.config.js');
+const WebpackDevServer = require('webpack-dev-server');
+const graphqlHTTP = require('express-graphql');
+const schema = require('./schema');
+
+// WebpackDevServer should only be used for dev
+const server = new WebpackDevServer(webpack(webpackConfig), {
+ hot: true,
+ historyApiFallback: {
+ index: './static/index.html'
+ },
+ setup: function(app) {
+ app.use('/graphql', graphqlHTTP({
+ schema: schema,
+ graphiql: true
+ }));
+ },
+ publicPath: webpackConfig.output.publicPath
+});
+
+server.listen(3000, 'localhost', (err) => {
+ if (err) {
+ console.log(err);
+ }
+
+ console.log('Listening at localhost:3000');
+});
diff --git a/spikes/stacks/redux-loop/src/server/schema.js b/spikes/stacks/redux-loop/src/server/schema.js
new file mode 100644
index 00000000..521d370b
--- /dev/null
+++ b/spikes/stacks/redux-loop/src/server/schema.js
@@ -0,0 +1,142 @@
+const changes = require('./data/changes.json');
+const products = require('./data/products.json');
+const values = require('lodash.values');
+
+const {
+ GraphQLString,
+ GraphQLObjectType,
+ GraphQLInt,
+ GraphQLID,
+ GraphQLList,
+ GraphQLSchema
+} = require('graphql');
+
+const ProductType = new GraphQLObjectType({
+ name: 'ProductType',
+ fields: {
+ id: {
+ type: GraphQLID
+ },
+ artist: {
+ type: GraphQLString
+ },
+ title: {
+ type: GraphQLString
+ },
+ label: {
+ type: GraphQLString
+ },
+ format: {
+ type: GraphQLString
+ },
+ price: {
+ type: GraphQLInt
+ },
+ currency: {
+ type: GraphQLString
+ }
+ }
+});
+
+const ChangeType = new GraphQLObjectType({
+ name: 'ChangeType',
+ fields: {
+ id: {
+ type: GraphQLID
+ },
+ product: {
+ type: ProductType,
+ resolve: (root, args) => {
+ return products[root.product];
+ }
+ },
+ price: {
+ type: GraphQLInt
+ },
+ currency: {
+ type: GraphQLString
+ }
+ }
+});
+
+const query = new GraphQLObjectType({
+ name: 'RootQueryType',
+ fields: {
+ products: {
+ type: new GraphQLList(ProductType),
+ args: {
+ id: {
+ type: GraphQLID
+ }
+ },
+ resolve(root, args, ctx) {
+ return args.id ? [products[args.id]] : values(products);
+ }
+ },
+ changes: {
+ type: new GraphQLList(ChangeType),
+ args: {
+ id: {
+ type: GraphQLID
+ },
+ product: {
+ type: GraphQLID
+ }
+ },
+ resolve(root, args, ctx) {
+ if (args.id) {
+ return [changes[args.id]];
+ }
+
+ if (!args.product) {
+ return values(changes);
+ }
+
+ return values(changes).filter((change) => {
+ return change.product === args.product;
+ });
+ }
+ }
+ }
+});
+
+const mutation = new GraphQLObjectType({
+ name: 'RootMutationType',
+ fields: {
+ removeChange: {
+ type: ChangeType,
+ args: {
+ id: {
+ type: GraphQLID
+ },
+ product: {
+ type: GraphQLID
+ }
+ },
+ resolve(root, args, ctx) {
+ const ops = (() => {
+ if (args.id) {
+ return [args.id];
+ }
+
+ if (!args.product) {
+ return Object.keys(changes);
+ }
+
+ return Object.keys(changes).filter((id) => {
+ return changes[id].product === args.product;
+ });
+ })();
+
+ ops.forEach((id) => {
+ delete changes[id];
+ });
+ }
+ }
+ }
+});
+
+module.exports = new GraphQLSchema({
+ query,
+ mutation
+});
diff --git a/spikes/stacks/redux-loop/static/index.html b/spikes/stacks/redux-loop/static/index.html
new file mode 100644
index 00000000..d36d2c24
--- /dev/null
+++ b/spikes/stacks/redux-loop/static/index.html
@@ -0,0 +1,10 @@
+
+
+
+ React Boilerplate
+
+
+
+
+
+
diff --git a/spikes/stacks/redux-loop/stories/index.js b/spikes/stacks/redux-loop/stories/index.js
new file mode 100644
index 00000000..3b098180
--- /dev/null
+++ b/spikes/stacks/redux-loop/stories/index.js
@@ -0,0 +1,10 @@
+const React = require('react');
+const Storybook = require('@kadira/storybook');
+
+const Home = require('../src/client/containers/home');
+
+const homeStories = Storybook.storiesOf('Home', module);
+
+homeStories.add('with nothing', () => (
+
+));
diff --git a/spikes/stacks/redux-loop/test/index.js b/spikes/stacks/redux-loop/test/index.js
new file mode 100644
index 00000000..62cf17c3
--- /dev/null
+++ b/spikes/stacks/redux-loop/test/index.js
@@ -0,0 +1,13 @@
+const test = require('ava');
+const enzyme = require('enzyme');
+const React = require('react');
+
+const {
+ shallow
+} = enzyme;
+
+test('renders without exploding', (t) => {
+ const Home = require('../src/client/containers/home');
+ const wrapper = shallow();
+ t.deepEqual(wrapper.length, 1);
+});