├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── ddp-connector
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── package.json
└── src
│ ├── connectDDP.js
│ ├── createConnector.js
│ ├── debounceProps.js
│ ├── index.js
│ └── wrapSelector.js
├── ddp-redux
├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── README.md
├── package.json
└── src
│ ├── DDPClient.js
│ ├── DDPClient.test.js
│ ├── DDPEmitter.js
│ ├── DDPEmitter.test.js
│ ├── DDPError.js
│ ├── DDPSocket.js
│ ├── DDPSocket.test.js
│ ├── actions.js
│ ├── constants.js
│ ├── ejson
│ ├── custom_models_for_tests.js
│ ├── ejson.js
│ ├── ejson.test.js
│ ├── index.js
│ ├── newBinary.js
│ ├── stringify.js
│ └── tinytest.js
│ ├── index.js
│ ├── modules
│ ├── collections
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ ├── selectors.test.js
│ │ └── testCommon.js
│ ├── connection
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── currentUser
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── messages
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── methods
│ │ ├── helpers.js
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── queries
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── queries2
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── queues
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ └── selectors.js
│ ├── resources
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ └── selectors.js
│ ├── subscriptions
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ ├── reducer.js
│ │ ├── reducer.test.js
│ │ ├── selectors.js
│ │ └── testCommon.js
│ ├── thunk.js
│ └── wrapWithPromise
│ │ ├── index.js
│ │ ├── middleware.js
│ │ ├── middleware.test.js
│ │ └── testCommon.js
│ ├── shim.js
│ └── utils
│ ├── Storage.js
│ ├── carefullyMapValues.js
│ ├── carefullyMapValues.test.js
│ ├── createDelayedTask.js
│ ├── createInsertEntities.js
│ ├── createRemoveEntities.js
│ ├── createValuesMappingSelector.js
│ ├── createValuesMappingSelector.test.js
│ ├── memoizeValuesMapping.js
│ ├── memoizeValuesMapping.test.js
│ ├── sha256.js
│ └── stableMap.js
└── example
├── .tmux.conf
├── backend
├── .eslintrc.js
├── .gitignore
├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
├── imports
│ ├── api
│ │ ├── TodoLists.js
│ │ ├── Todos.js
│ │ ├── implement.js
│ │ ├── index.js
│ │ └── publish.js
│ ├── collections
│ │ ├── TodoLists.js
│ │ └── Todos.js
│ ├── common
│ └── schema
│ │ ├── CreatedUpdatedSchema.js
│ │ └── autoValue.js
├── package-lock.json
├── package.json
└── server
│ └── main.js
├── start-develop.sh
├── start.sh
└── todo-web
├── .env
├── .eslintrc.json
├── .gitignore
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── common
├── .gitignore
├── api
│ ├── TodoLists.js
│ └── Todos.js
├── models
│ ├── BaseModel.js
│ ├── Todo.js
│ └── TodoList.js
└── utils
│ ├── ApiSpec.js
│ ├── Schema.js
│ └── errors.js
├── components
├── Loader.js
└── NotFound.js
├── containers
├── App.js
├── App.test.js
├── Entry.js
├── List.js
├── Lists.js
└── LoggedInRoute.js
├── index.css
├── index.js
├── registerServiceWorker.js
├── routes
└── Router.jsx
├── storage.js
└── store
└── rootReducer.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | install:
3 | - (cd ddp-redux && npm install)
4 | script:
5 | - (cd ddp-redux && npm run test && npm run test-lib)
6 | node_js:
7 | - "4"
8 | - "6"
9 | - "8"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Tomasz Lenarcik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ddp
2 |
3 | [![Build Status][travis-svg]][travis-url]
4 |
5 | `ddp-redux` is a brand new ddp client that will allow you to fetch resources from your ddp server
6 | in a completely declarative way. Subscriptions/ methods (queries) parameters are evaluated by
7 | selectors and all you need to do is to extract collection data from the redux store.
8 | This approach was highly inspired by [apollo-client][apollo-client-url], but the DDP protocol
9 | is the first class citizen in our case.
10 |
11 | An example is worth a thousand words
12 | ```javascript
13 | import ddp from 'ddp-connector';
14 |
15 | const TodoList = ddp({
16 | subscriptions: (state, props) => [{
17 | name: 'api.collections.TodoLists.details',
18 | params: [{
19 | listId: props.listId,
20 | }],
21 | }, {
22 | name: 'api.collections.Todos.all',
23 | params: [{
24 | listId: props.listId,
25 | }],
26 | }],
27 | selectors: ({ from, prop }) => ({
28 | todoList: from('TodoLists').select.one('listId'),
29 | todos: from('Todos').select.where(
30 | createSelector(
31 | prop('listId'),
32 | listId => todo => todo.listId === listId,
33 | ),
34 | ),
35 | }),
36 | })(({ todoList, todos }) => (
37 |
38 |
{todoList.name}
39 |
40 | {todos.map(todo => (- {todo.title}
))}
41 |
42 |
43 | ))
44 | ```
45 | Most typical DDP "commands" are accessible by simply dispatching a redux action, e.g.
46 | ```javascript
47 | import { callMethod } from 'ddp-redux';
48 |
49 | // ...
50 |
51 | store.dispatch(
52 | callMethod(
53 | 'myTestMethod',
54 | param1,
55 | param2,
56 | param3,
57 | ))
58 | .then(/* ... */)
59 | .catch(/* ... */)
60 | );
61 | ```
62 |
63 | ## Disclaimer
64 |
65 | The project is still in a pretty early stage and it does not have proper documentation yet,
66 | as some parts of the api are very likely to change, e.g. documents selectors.
67 |
68 | ## Running example app
69 |
70 | Assuming yoy have `tmux@2.x` installed you only need to run
71 | ```
72 | cd example
73 | ./start.sh
74 | ```
75 | If you want to play with the code and need to ensure that the libraries
76 | are rebuilding automatically use
77 | ```
78 | ./start-develop.sh
79 | ```
80 |
81 | ## Installation
82 |
83 | ```
84 | npm install --save ddp-redux ddp-connector
85 | ```
86 | If you don't have them already please install the following peer dependencies
87 | ```
88 | npm install --save react redux react-redux
89 | ```
90 |
91 | ## Minimal configuration
92 |
93 | ```javascript
94 | import { createStore, combineReducers, applyMiddleware } from 'redux';
95 | import React from 'react';
96 | import ReactDOM from 'react-dom';
97 | import { Provider } from 'react-redux';
98 | import DDPClient from 'ddp-redux';
99 |
100 | const ddpClient = new DDPClient({
101 | endpoint: 'ws://localhost:3000/websocket',
102 | SocketConstructor: WebSocket,
103 | });
104 |
105 | const reducer = combineReducers({
106 | ddp: DDPClient.reducer(),
107 | });
108 |
109 | const store = createStore(
110 | reducer,
111 | {},
112 | applyMiddleware(
113 | ddpClient.middleware(),
114 | ),
115 | );
116 |
117 | ReactDOM.render(
118 |
119 | // ...
120 |
121 | document.getElementById('root'),
122 | );
123 | ```
124 |
125 | ## Features
126 |
127 | - [x] Supports Meteor standard authentication methods
128 | - [x] Meteor subscriptions
129 | - [x] Fetch data via meteor methods
130 | - [x] Aggregate data from multiple ddp connections
131 | - [x] Basic support for optimistic updates
132 | - [ ] Sync local ids generator with server
133 | - [ ] Out-of-the-box support for client-side joins
134 | - [ ] Offline first
135 |
136 | [travis-svg]: https://travis-ci.org/apendua/ddp-redux.svg?branch=develop
137 | [travis-url]: https://travis-ci.org/apendua/ddp-redux
138 | [apollo-client-url]: https://github.com/apollographql/apollo-client
--------------------------------------------------------------------------------
/ddp-connector/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-3", "flow"],
3 | "plugins": ["syntax-trailing-function-commas"]
4 | }
--------------------------------------------------------------------------------
/ddp-connector/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "import/prefer-default-export": "off"
5 | },
6 | "parser": "babel-eslint",
7 | "parserOptions": {
8 | "ecmaVersion": 7,
9 | "sourceType": "module",
10 | "ecmaFeatures": {
11 | "experimentalObjectRestSpread": true
12 | }
13 | },
14 | "env": {
15 | "es6": true,
16 | "browser": true,
17 | "node": true
18 | },
19 | "plugins": [],
20 | "settings": {}
21 | }
--------------------------------------------------------------------------------
/ddp-connector/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | /node_modules
3 | /package-lock.json
4 | /npm-debug.log
--------------------------------------------------------------------------------
/ddp-connector/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 |
--------------------------------------------------------------------------------
/ddp-connector/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ddp-connector",
3 | "version": "0.5.0",
4 | "description": "Connect ddp-redux client to react components",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "test-watch": "jest --watch",
9 | "coverage": "jest --coverage",
10 | "build": "babel src/ -d lib/",
11 | "build-watch": "watch 'npm run build' ./src",
12 | "prepublish": "npm run build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/apendua/ddp.git"
17 | },
18 | "keywords": [
19 | "meteor",
20 | "ddp",
21 | "redux",
22 | "react"
23 | ],
24 | "author": "apendua",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/apendua/ddp/issues"
28 | },
29 | "homepage": "https://github.com/apendua/ddp#readme",
30 | "dependencies": {
31 | "lodash": "^4.17.4",
32 | "prop-types": "^15.6.0",
33 | "recompose": "^0.26.0",
34 | "reselect": "^3.0.1"
35 | },
36 | "peerDependencies": {
37 | "ddp-redux": "^0.0.4",
38 | "react": "^15.0.0-0 || ^16.0.0-0",
39 | "react-redux": "^5.0.6"
40 | },
41 | "devDependencies": {
42 | "babel-cli": "^6.24.1",
43 | "babel-core": "^6.25.0",
44 | "babel-eslint": "^7.2.3",
45 | "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
46 | "babel-preset-es2015": "^6.24.1",
47 | "babel-preset-flow": "^6.23.0",
48 | "babel-preset-stage-0": "^6.24.1",
49 | "babel-preset-stage-3": "^6.24.1",
50 | "eslint": "^4.3.0",
51 | "eslint-config-airbnb": "^15.1.0",
52 | "eslint-import-resolver-meteor": "^0.4.0",
53 | "eslint-plugin-import": "^2.7.0",
54 | "eslint-plugin-jsx-a11y": "^5.1.1",
55 | "eslint-plugin-react": "^7.1.0",
56 | "jest": "^23.1.0",
57 | "watch": "^1.0.2"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ddp-connector/src/debounceProps.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import forEach from 'lodash/forEach';
3 | import {
4 | wrapDisplayName,
5 | setDisplayName,
6 | } from 'recompose';
7 |
8 | /**
9 | * @param {String} name - name of the property to debounce
10 | * @param {Number} ms - number of ms to ms the property update
11 | * @returns {HigherOrderComponent}
12 | */
13 | const debounceProps = (names, {
14 | ms = 200,
15 | } = {}) => {
16 | const hoc = (BaseComponent) => {
17 | /**
18 | * A component that adds "autosave" function to properties. That function can be called with new changes object
19 | * and the actual call to "onAutosave" will be postponed by the number of milliseconds given by autosaveDelay.
20 | */
21 | class Container extends React.Component {
22 | constructor(props) {
23 | super(props);
24 | const initialState = {};
25 | forEach(names, (name) => {
26 | initialState[name] = props[name];
27 | });
28 | this.state = initialState;
29 | this.timeout = null;
30 | }
31 |
32 | /**
33 | * Schedule updating component.
34 | * @private
35 | */
36 | componentWillReceiveProps(nextProps) {
37 | if (this.timeout) {
38 | clearTimeout(this.timeout);
39 | }
40 | this.timeout = setTimeout(this.update.bind(this, nextProps), ms);
41 | }
42 |
43 | /**
44 | * Cancel pending timeout.
45 | * @private
46 | */
47 | componentWillUnmount() {
48 | if (this.timeout) {
49 | clearTimeout(this.timeout);
50 | }
51 | this.timeout = null;
52 | }
53 |
54 | update(props) {
55 | const newState = {};
56 | forEach(names, (name) => {
57 | newState[name] = props[name];
58 | });
59 | this.setState(newState);
60 | this.timeout = null;
61 | }
62 |
63 | render() {
64 | const props = {
65 | ...this.props,
66 | };
67 | forEach(names, (name) => {
68 | props[name] = this.state[name];
69 | });
70 | return React.createElement(BaseComponent, props);
71 | }
72 | }
73 | return Container;
74 | };
75 | if (process.env.NODE_ENV !== 'production') {
76 | return BaseComponent => setDisplayName(wrapDisplayName(BaseComponent, 'debounceProps'))(hoc(BaseComponent));
77 | }
78 | return hoc;
79 | };
80 |
81 | export default debounceProps;
82 |
--------------------------------------------------------------------------------
/ddp-connector/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import mapValues from 'lodash/mapValues';
3 | import isArray from 'lodash/isArray';
4 | import createConnector from './createConnector';
5 |
6 | const identity = x => x;
7 |
8 | const ddp = createConnector({
9 | defaultMapQueries: ({
10 | queriesOptions,
11 | }) => (queries) => {
12 | if (isArray(queries)) {
13 | return {};
14 | }
15 | return mapValues(queries, (query, key) => {
16 | if (!query) {
17 | return query;
18 | }
19 | const options = queriesOptions && queriesOptions[key];
20 | const mapResult = (options && options.mapResult) || identity;
21 | return mapResult(query.result);
22 | });
23 | },
24 | defaultLoader: () => React.createElement('span', {}, 'Loading ...'),
25 | defaultDebounceReady: 100,
26 | });
27 |
28 | export {
29 | ddp,
30 | createConnector,
31 | };
32 |
33 | export default ddp;
34 |
--------------------------------------------------------------------------------
/ddp-connector/src/wrapSelector.js:
--------------------------------------------------------------------------------
1 | import {
2 | createSelectorCreator,
3 | defaultMemoize,
4 | } from 'reselect';
5 | import { EJSON } from 'ddp-redux';
6 |
7 | const identity = x => x;
8 | const constant = x => () => x;
9 |
10 | const createSelector = createSelectorCreator(
11 | defaultMemoize,
12 | EJSON.equals,
13 | );
14 |
15 | /**
16 | * Create a function that compares the computed value with the previous one
17 | * and if they're deeply equal, it returns the previous result.
18 | */
19 | const memoize = x => createSelector(x, identity);
20 |
21 | const wrapSelector = (selector) => {
22 | let compiled;
23 | // Make sure that the function is only called with the arguments it needs.
24 | // This is important when the function uses arguments based caching
25 | // to optimize it's computations.
26 | const compile = (func) => {
27 | // if (func.length >= 2) {
28 | // return memoize(func);
29 | // } else if (func.length === 1) {
30 | // return memoize(state => func(state));
31 | // }
32 | // console.log('no arguments at all', func.toString());
33 | // return memoize(() => func());
34 | return memoize(func);
35 | };
36 | return (state, ownProps) => {
37 | if (compiled) {
38 | return compiled(state, ownProps);
39 | }
40 | if (typeof selector !== 'function') {
41 | compiled = constant(selector);
42 | return compiled();
43 | }
44 | const props = selector(state, ownProps);
45 | if (typeof props === 'function') {
46 | compiled = compile(props);
47 | return compiled(state, ownProps);
48 | }
49 | compiled = compile(selector);
50 | return props;
51 | };
52 | };
53 |
54 | export default wrapSelector;
55 |
--------------------------------------------------------------------------------
/ddp-redux/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-3", "flow"],
3 | "plugins": ["syntax-trailing-function-commas"]
4 | }
--------------------------------------------------------------------------------
/ddp-redux/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "import/prefer-default-export": "off",
5 | "import/extensions": "off",
6 | "max-len": ["error", 140]
7 | },
8 | "parser": "babel-eslint",
9 | "parserOptions": {
10 | "ecmaVersion": 7,
11 | "sourceType": "module",
12 | "ecmaFeatures": {
13 | "experimentalObjectRestSpread": true
14 | }
15 | },
16 | "env": {
17 | "es6": true,
18 | "browser": true,
19 | "node": true
20 | },
21 | "plugins": [],
22 | "settings": {}
23 | }
24 |
--------------------------------------------------------------------------------
/ddp-redux/.gitignore:
--------------------------------------------------------------------------------
1 | /lib
2 | /node_modules
3 | /package-lock.json
4 | /npm-debug.log
5 | /coverage
6 |
--------------------------------------------------------------------------------
/ddp-redux/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 |
--------------------------------------------------------------------------------
/ddp-redux/README.md:
--------------------------------------------------------------------------------
1 | # ddp-redux
2 |
--------------------------------------------------------------------------------
/ddp-redux/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ddp-redux",
3 | "version": "0.5.0",
4 | "description": "A redux based DDP client",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "test-watch": "jest --watch",
9 | "coverage": "jest --coverage",
10 | "build": "babel src/ -d lib/",
11 | "build-watch": "watch 'npm run build' ./src",
12 | "lint": "eslint src/",
13 | "prepublish": "npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/apendua/ddp.git"
18 | },
19 | "keywords": [
20 | "meteor",
21 | "ddp",
22 | "redux",
23 | "websocket"
24 | ],
25 | "author": "apendua",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/apendua/ddp/issues"
29 | },
30 | "homepage": "https://github.com/apendua/ddp#readme",
31 | "devDependencies": {
32 | "babel-cli": "^6.26.0",
33 | "babel-core": "^6.26.3",
34 | "babel-eslint": "^7.2.3",
35 | "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
36 | "babel-preset-es2015": "^6.24.1",
37 | "babel-preset-flow": "^6.23.0",
38 | "babel-preset-stage-0": "^6.24.1",
39 | "babel-preset-stage-3": "^6.24.1",
40 | "chai": "^4.1.2",
41 | "eslint": "^4.19.1",
42 | "eslint-config-airbnb": "^16.1.0",
43 | "eslint-import-resolver-meteor": "^0.4.0",
44 | "eslint-plugin-import": "^2.12.0",
45 | "eslint-plugin-jsx-a11y": "^6.0.3",
46 | "eslint-plugin-react": "^7.9.1",
47 | "jest": "^23.1.0",
48 | "redux": "^3.7.2",
49 | "redux-mock-store": "^1.2.3",
50 | "watch": "^1.0.2"
51 | },
52 | "dependencies": {
53 | "base64-js": "^1.2.1",
54 | "lodash": "^4.17.10",
55 | "regexp.prototype.flags": "^1.2.0",
56 | "reselect": "^3.0.1",
57 | "shallowequal": "^1.0.2"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPClient.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import configureStore from 'redux-mock-store';
4 | import DDPClient from './DDPClient';
5 |
6 | describe('Test DDPClient', () => {
7 | let testContext;
8 |
9 | beforeEach(() => {
10 | testContext = {};
11 | });
12 |
13 | beforeEach(() => {
14 | testContext.ddpClient = new DDPClient();
15 | });
16 |
17 | describe('Given I have a ddp middleware', () => {
18 | beforeEach(() => {
19 | testContext.middleware = testContext.ddpClient.middleware();
20 | testContext.mockStore = configureStore([
21 | testContext.middleware,
22 | ]);
23 | });
24 |
25 | test('should accept function as an action', () => {
26 | const store = testContext.mockStore();
27 | store.dispatch((dispatch) => {
28 | dispatch({
29 | type: 'test_action',
30 | });
31 | });
32 | expect(store.getActions()).toEqual(expect.arrayContaining([
33 | { type: 'test_action' },
34 | ]));
35 | });
36 | });
37 |
38 | test('should be ok', () => {
39 | expect(testContext.ddpClient).toBeTruthy();
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPEmitter.js:
--------------------------------------------------------------------------------
1 | import forEach from 'lodash/forEach';
2 |
3 | const listeners = new WeakMap();
4 |
5 | class DDPEmitter {
6 | constructor() {
7 | listeners.set(this, {});
8 | }
9 |
10 | on(name, callback) {
11 | const thisListeners = listeners.get(this);
12 | if (!thisListeners[name]) {
13 | thisListeners[name] = [];
14 | }
15 | thisListeners[name].push(callback);
16 | }
17 |
18 | emit(name, ...args) {
19 | forEach(listeners.get(this)[name], callback => callback(...args));
20 | }
21 | }
22 |
23 | export default DDPEmitter;
24 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPEmitter.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import DDPEmitter from './DDPEmitter';
4 |
5 | describe('Test DDPEmitter', () => {
6 | let testContext;
7 |
8 | beforeEach(() => {
9 | testContext = {};
10 | });
11 |
12 | beforeEach(() => {
13 | testContext.emitter = new DDPEmitter();
14 | testContext.m1 = jest.fn();
15 | testContext.m2 = jest.fn();
16 | testContext.m3 = jest.fn();
17 | testContext.emitter.on('m1', testContext.m1);
18 | testContext.emitter.on('m2', testContext.m2);
19 | testContext.emitter.on('m2', testContext.m3);
20 | });
21 |
22 | test('should not do anyghing there are no listeners', () => {
23 | testContext.emitter.emit('mx');
24 | expect(testContext.m1).not.toBeCalled();
25 | expect(testContext.m2).not.toBeCalled();
26 | expect(testContext.m3).not.toBeCalled();
27 | });
28 |
29 | test('should trigger one listener', () => {
30 | testContext.emitter.emit('m1');
31 | expect(testContext.m1).toBeCalled();
32 | expect(testContext.m2).not.toBeCalled();
33 | expect(testContext.m3).not.toBeCalled();
34 | });
35 |
36 | test('should trigger two listeners', () => {
37 | testContext.emitter.emit('m2');
38 | expect(testContext.m1).not.toBeCalled();
39 | expect(testContext.m2).toBeCalled();
40 | expect(testContext.m3).toBeCalled();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPError.js:
--------------------------------------------------------------------------------
1 | // Based on Meteor's EJSON implementation: https://github.com/meteor/meteor
2 | //
3 | // packages/meteor/errors.js
4 | // 2017-08-10
5 | //
6 | // The MIT License (MIT)
7 | //
8 | // Copyright (c) 2011 - 2017 Meteor Development Group, Inc.
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 |
28 | class DDPError extends Error {
29 | constructor(error, reason, details) {
30 | super();
31 |
32 | // Ensure we get a proper stack trace in most Javascript environments
33 | if (Error.captureStackTrace) {
34 | // V8 environments (Chrome and Node.js)
35 | Error.captureStackTrace(this, DDPError);
36 | } else {
37 | // Borrow the .stack property of a native Error object.
38 | this.stack = new Error().stack;
39 | }
40 | // Safari magically works.
41 |
42 | // Newer versions of DDP use this property to signify that an error
43 | // can be sent back and reconstructed on the calling client.
44 | this.isClientSafe = true;
45 |
46 | // String code uniquely identifying this kind of error.
47 | this.error = error;
48 |
49 | // Optional: A short human-readable summary of the error. Not
50 | // intended to be shown to end users, just developers. ("Not Found",
51 | // "Internal Server Error")
52 | this.reason = reason;
53 |
54 | // Optional: Additional information about the error, say for
55 | // debugging. It might be a (textual) stack trace if the server is
56 | // willing to provide one. The corresponding thing in HTTP would be
57 | // the body of a 404 or 500 response. (The difference is that we
58 | // never expect this to be shown to end users, only developers, so
59 | // it doesn't need to be pretty.)
60 | this.details = details;
61 |
62 | // This is what gets displayed at the top of a stack trace. Current
63 | // format is "[404]" (if no reason is set) or "File not found [404]"
64 | if (this.reason) {
65 | this.message = `${this.reason} [${this.error}]`;
66 | } else {
67 | this.message = `[${this.error}]`;
68 | }
69 |
70 | this.errorType = 'Meteor.Error';
71 | }
72 |
73 | // Meteor.Error is basically data and is sent over DDP, so you should be able to
74 | // properly EJSON-clone it. This is especially important because if a
75 | // Meteor.Error is thrown through a Future, the error, reason, and details
76 | // properties become non-enumerable so a standard Object clone won't preserve
77 | // them and they will be lost from DDP.
78 | clone() {
79 | return new DDPError(this.error, this.reason, this.details);
80 | }
81 | }
82 |
83 | DDPError.ERROR_CONNECTION = 'ErrorConnection';
84 | DDPError.ERROR_BAD_MESSAGE = 'ErrorBadMessage';
85 |
86 | export default DDPError;
87 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPSocket.js:
--------------------------------------------------------------------------------
1 | import EJSON from './ejson';
2 | import DDPEmitter from './DDPEmitter';
3 |
4 | class DDPScoket extends DDPEmitter {
5 | constructor({
6 | SocketConstructor,
7 | }) {
8 | super();
9 |
10 | this.SocketConstructor = SocketConstructor;
11 | this.rawSocket = null;
12 | }
13 |
14 | send(obj) {
15 | // console.warn('ddp:out', obj);
16 | this.rawSocket.send(EJSON.stringify(obj));
17 | }
18 |
19 | open(endpoint) {
20 | if (this.rawSocket) {
21 | throw new Error('Socket already opened');
22 | }
23 | this.rawSocket = new this.SocketConstructor(endpoint);
24 |
25 | this.rawSocket.onopen = () => {
26 | this.emit('open');
27 | };
28 |
29 | this.rawSocket.onclose = () => {
30 | this.rawSocket = null;
31 | this.emit('close');
32 | };
33 |
34 | this.rawSocket.onerror = () => {
35 | // NOTE: Overwrite the "onclose" hook to prevent emitting "close" event twice.
36 | // Please note that "delete this.rawSocket.onclose" does not work in this case.
37 | this.rawSocket.onclose = null;
38 | this.rawSocket.close();
39 | this.rawSocket = null;
40 | this.emit('close');
41 | };
42 |
43 | this.rawSocket.onmessage = (message) => {
44 | let obj;
45 | try {
46 | obj = EJSON.parse(message.data);
47 | } catch (err) {
48 | // ignore for now
49 | }
50 | // console.warn('ddp:in', obj);
51 | this.emit('message', obj);
52 | };
53 | }
54 |
55 | close() {
56 | if (this.rawSocket) {
57 | this.rawSocket.close();
58 | this.rawSocket = null;
59 | }
60 | }
61 | }
62 |
63 | export default DDPScoket;
64 |
--------------------------------------------------------------------------------
/ddp-redux/src/DDPSocket.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import DDPSocket from './DDPSocket';
4 |
5 | class Socket {
6 | constructor(endpoint) {
7 | this.messages = [];
8 | this.endpoint = endpoint;
9 | }
10 |
11 | close() {
12 | if (this.onclose) {
13 | this.onclose();
14 | }
15 | }
16 |
17 | send(message) {
18 | this.messages.push(message);
19 | }
20 |
21 | triggerMessage(message) {
22 | if (this.onmessage) {
23 | this.onmessage({ data: message });
24 | }
25 | }
26 |
27 | triggerOpen() {
28 | if (this.onopen) {
29 | this.onopen();
30 | }
31 | }
32 | }
33 |
34 | describe('Test DDPSocket', () => {
35 | let testContext;
36 |
37 | beforeEach(() => {
38 | testContext = {};
39 | });
40 |
41 | beforeEach(() => {
42 | testContext.socket = new DDPSocket({
43 | endpoint: 'ws://example.com',
44 | SocketConstructor: Socket,
45 | });
46 | testContext.onMessage = jest.fn();
47 | testContext.onClose = jest.fn();
48 | testContext.onOpen = jest.fn();
49 | testContext.socket.on('message', testContext.onMessage);
50 | testContext.socket.on('close', testContext.onClose);
51 | testContext.socket.on('open', testContext.onOpen);
52 | testContext.socket.open('ws://example.com');
53 | });
54 |
55 | test('should trigger onOpen callback', () => {
56 | testContext.socket.rawSocket.triggerOpen();
57 | expect(testContext.onOpen).toBeCalled();
58 | });
59 |
60 | test('should trigger onClose callback', () => {
61 | testContext.socket.close();
62 | expect(testContext.onClose).toBeCalled();
63 | });
64 |
65 | test('should connect to the right endpoint', () => {
66 | expect(testContext.socket.rawSocket.endpoint).toBe('ws://example.com');
67 | });
68 |
69 | test('should send a stringified DDP message', () => {
70 | testContext.socket.send({
71 | msg: 'connect',
72 | });
73 | expect(testContext.socket.rawSocket.messages).toEqual(expect.arrayContaining([
74 | '{"msg":"connect"}',
75 | ]));
76 | });
77 |
78 | test('should receive a parsed DDP message', () => {
79 | testContext.socket.rawSocket.triggerMessage('{"msg":"ping"}');
80 | expect(testContext.onMessage).toBeCalledWith({ msg: 'ping' });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/ddp-redux/src/actions.js:
--------------------------------------------------------------------------------
1 | import forEach from 'lodash/forEach';
2 | import {
3 | DDP_OPEN,
4 | DDP_CLOSE,
5 | DDP_METHOD,
6 | DDP_SUBSCRIBE,
7 | DDP_UNSUBSCRIBE,
8 | DDP_QUERY_REQUEST,
9 | DDP_QUERY_RELEASE,
10 | DDP_QUERY_REFETCH,
11 | DDP_QUERY_UPDATE,
12 |
13 | DDP_RESOURCE_FETCH,
14 | DDP_RESOURCE_CREATE,
15 | DDP_RESOURCE_DELETE,
16 | DDP_RESOURCE_REFETCH,
17 | DDP_RESOURCE_DEPRECATE,
18 | DDP_RESOURCE_UPDATE,
19 |
20 | DDP_LOGIN,
21 | DDP_LOGOUT,
22 | } from './constants';
23 | import sha256 from './utils/sha256';
24 |
25 | const hashPassword = password => ({
26 | digest: sha256(password),
27 | algorithm: 'sha-256',
28 | });
29 |
30 | export const openSocket = (endpoint, params, meta) => ({
31 | type: DDP_OPEN,
32 | payload: {
33 | endpoint,
34 | params,
35 | },
36 | ...meta && { meta },
37 | });
38 |
39 | export const closeSocket = (socketId, meta) => ({
40 | type: DDP_CLOSE,
41 | meta: {
42 | ...meta,
43 | socketId,
44 | },
45 | });
46 |
47 | export const callMethod = (name, params, meta) => ({
48 | type: DDP_METHOD,
49 | payload: {
50 | params,
51 | method: name,
52 | },
53 | ...meta && { meta },
54 | });
55 |
56 | export const subscribe = (name, params, meta) => ({
57 | type: DDP_SUBSCRIBE,
58 | payload: {
59 | name,
60 | params,
61 | },
62 | ...meta && { meta },
63 | });
64 |
65 | export const unsubscribe = (subId, meta) => ({
66 | type: DDP_UNSUBSCRIBE,
67 | meta: {
68 | ...meta,
69 | subId,
70 | },
71 | });
72 |
73 | export const queryRequest = (name, params, properties) => ({
74 | type: DDP_QUERY_REQUEST,
75 | payload: {
76 | name,
77 | params,
78 | properties,
79 | },
80 | });
81 |
82 | export const queryRelease = (queryId, meta) => ({
83 | type: DDP_QUERY_RELEASE,
84 | meta: {
85 | ...meta,
86 | queryId,
87 | },
88 | });
89 |
90 |
91 | export const queryRefetch = (queryId, meta) => ({
92 | type: DDP_QUERY_REFETCH,
93 | meta: {
94 | ...meta,
95 | queryId,
96 | },
97 | });
98 |
99 | export const queryUpdate = (queryId, payload) => ({
100 | type: DDP_QUERY_UPDATE,
101 | payload,
102 | meta: {
103 | queryId,
104 | },
105 | });
106 |
107 | // NOTE: Do not use this method in general!
108 | export const queryRefetchAll = () => (dispatch, getState) => {
109 | const state = getState();
110 | forEach(state.ddp.queries, (qyery, queryId) => {
111 | dispatch(queryRefetch(queryId));
112 | });
113 | };
114 |
115 | // RESOURCES
116 |
117 | export const fetchResource = (resourceId, payload) => ({
118 | type: DDP_RESOURCE_FETCH,
119 | payload,
120 | meta: {
121 | resourceId,
122 | },
123 | });
124 |
125 | export const refetchResource = (resourceId, payload) => ({
126 | type: DDP_RESOURCE_REFETCH,
127 | payload,
128 | meta: {
129 | resourceId,
130 | },
131 | });
132 |
133 | export const createResource = (name, params, properties) => ({
134 | type: DDP_RESOURCE_CREATE,
135 | payload: {
136 | name,
137 | params,
138 | properties,
139 | },
140 | });
141 |
142 | export const deleteResource = resourceId => ({
143 | type: DDP_RESOURCE_DELETE,
144 | meta: {
145 | resourceId,
146 | },
147 | });
148 |
149 | export const updateResource = (resourceId, payload) => ({
150 | type: DDP_RESOURCE_UPDATE,
151 | payload,
152 | meta: {
153 | resourceId,
154 | },
155 | });
156 |
157 | export const deprecateResource = resourceId => ({
158 | type: DDP_RESOURCE_DEPRECATE,
159 | meta: {
160 | resourceId,
161 | },
162 | });
163 |
164 | export const login = (params, meta) => ({
165 | type: DDP_LOGIN,
166 | payload: [params],
167 | ...meta && { meta },
168 | });
169 |
170 | export const logout = meta => ({
171 | type: DDP_LOGOUT,
172 | ...meta && { meta },
173 | });
174 |
175 | export const loginWithPassword = ({
176 | username,
177 | email,
178 | password,
179 | }, meta) => login({
180 | user: {
181 | username,
182 | email,
183 | },
184 | password: hashPassword(password),
185 | }, meta);
186 |
187 | export const createUser = ({
188 | password,
189 | ...rest
190 | }, meta) => ({
191 | type: DDP_LOGIN,
192 | payload: [{
193 | password: hashPassword(password),
194 | ...rest,
195 | }],
196 | meta: {
197 | ...meta,
198 | method: 'createUser',
199 | },
200 | });
201 |
202 | export const resetPassword = ({
203 | token,
204 | newPassword,
205 | }, meta) => ({
206 | type: DDP_LOGIN,
207 | payload: [
208 | token,
209 | hashPassword(newPassword),
210 | ],
211 | meta: {
212 | ...meta,
213 | method: 'resetPassword',
214 | },
215 | });
216 |
217 | export const forgotPassword = ({
218 | email,
219 | }, meta) => callMethod('forgotPassword', [{ email }], meta);
220 |
--------------------------------------------------------------------------------
/ddp-redux/src/ejson/custom_models_for_tests.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // Based on Meteor's EJSON implementation: https://github.com/meteor/meteor
4 | //
5 | // packages/ejson/custom_models_for_tests.js
6 | // 2017-08-08
7 | //
8 | // The MIT License (MIT)
9 | //
10 | // Copyright (c) 2011 - 2017 Meteor Development Group, Inc.
11 | //
12 | // Permission is hereby granted, free of charge, to any person obtaining a copy
13 | // of this software and associated documentation files (the "Software"), to deal
14 | // in the Software without restriction, including without limitation the rights
15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 | // copies of the Software, and to permit persons to whom the Software is
17 | // furnished to do so, subject to the following conditions:
18 | //
19 | // The above copyright notice and this permission notice shall be included in all
20 | // copies or substantial portions of the Software.
21 | //
22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 | // SOFTWARE.
29 |
30 | import { EJSON } from './ejson';
31 |
32 | class Address {
33 | constructor(city, state) {
34 | this.city = city;
35 | this.state = state;
36 | }
37 |
38 | typeName() {
39 | return 'Address';
40 | }
41 |
42 | toJSONValue() {
43 | return {
44 | city: this.city,
45 | state: this.state,
46 | };
47 | }
48 | }
49 |
50 | EJSON.addType('Address', value => new Address(value.city, value.state));
51 |
52 | class Person {
53 | constructor(name, dob, address) {
54 | this.name = name;
55 | this.dob = dob;
56 | this.address = address;
57 | }
58 |
59 | typeName() {
60 | return 'Person';
61 | }
62 |
63 | toJSONValue() {
64 | return {
65 | name: this.name,
66 | dob: EJSON.toJSONValue(this.dob),
67 | address: EJSON.toJSONValue(this.address),
68 | };
69 | }
70 | }
71 |
72 | EJSON.addType(
73 | 'Person',
74 | value => new Person(
75 | value.name,
76 | EJSON.fromJSONValue(value.dob),
77 | EJSON.fromJSONValue(value.address)
78 | )
79 | );
80 |
81 | class Holder {
82 | constructor(content) {
83 | this.content = content;
84 | }
85 |
86 | typeName() {
87 | return 'Holder';
88 | }
89 |
90 | toJSONValue() {
91 | return this.content;
92 | }
93 | }
94 |
95 | EJSON.addType('Holder', value => new Holder(value));
96 |
97 | const EJSONTest = {
98 | Address,
99 | Person,
100 | Holder,
101 | };
102 |
103 | export default EJSONTest;
104 |
--------------------------------------------------------------------------------
/ddp-redux/src/ejson/index.js:
--------------------------------------------------------------------------------
1 | import { EJSON } from './ejson';
2 |
3 | export default EJSON;
4 |
--------------------------------------------------------------------------------
/ddp-redux/src/ejson/newBinary.js:
--------------------------------------------------------------------------------
1 | // Based on Meteor's EJSON implementation: https://github.com/meteor/meteor
2 | //
3 | // packages/ejson/ejson.js
4 | // 2017-08-08
5 | //
6 | // The MIT License (MIT)
7 | //
8 | // Copyright (c) 2011 - 2017 Meteor Development Group, Inc.
9 | //
10 | // Permission is hereby granted, free of charge, to any person obtaining a copy
11 | // of this software and associated documentation files (the "Software"), to deal
12 | // in the Software without restriction, including without limitation the rights
13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 | // copies of the Software, and to permit persons to whom the Software is
15 | // furnished to do so, subject to the following conditions:
16 | //
17 | // The above copyright notice and this permission notice shall be included in all
18 | // copies or substantial portions of the Software.
19 | //
20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 | // SOFTWARE.
27 |
28 | function newBinary(len) {
29 | if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') {
30 | const ret = [];
31 | for (let i = 0; i < len; i += 1) {
32 | ret.push(0);
33 | }
34 | ret.$Uint8ArrayPolyfill = true;
35 | return ret;
36 | }
37 | return new Uint8Array(new ArrayBuffer(len));
38 | }
39 |
40 | export default newBinary;
41 |
--------------------------------------------------------------------------------
/ddp-redux/src/ejson/stringify.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // Based on json2.js from https://github.com/douglascrockford/JSON-js
4 | //
5 | // json2.js
6 | // 2012-10-08
7 | //
8 | // Public Domain.
9 | //
10 | // NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
11 |
12 | function quote(string) {
13 | return JSON.stringify(string);
14 | }
15 |
16 | const str = (key, holder, singleIndent, outerIndent, canonical) => {
17 | const value = holder[key];
18 |
19 | // What happens next depends on the value's type.
20 | switch (typeof value) {
21 | case 'string':
22 | return quote(value);
23 | case 'number':
24 | // JSON numbers must be finite. Encode non-finite numbers as null.
25 | return isFinite(value) ? String(value) : 'null';
26 | case 'boolean':
27 | return String(value);
28 | // If the type is 'object', we might be dealing with an object or an array or
29 | // null.
30 | case 'object':
31 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
32 | // so watch out for that case.
33 | if (!value) {
34 | return 'null';
35 | }
36 | // Make an array to hold the partial results of stringifying this object
37 | // value.
38 | const innerIndent = outerIndent + singleIndent;
39 | const partial = [];
40 |
41 | let v;
42 |
43 | // Is the value an array?
44 | if (Array.isArray(value) || ({}).hasOwnProperty.call(value, 'callee')) {
45 | // The value is an array. Stringify every element. Use null as a
46 | // placeholder for non-JSON values.
47 | const length = value.length;
48 | for (let i = 0; i < length; i += 1) {
49 | partial[i] =
50 | str(i, value, singleIndent, innerIndent, canonical) || 'null';
51 | }
52 |
53 | // Join all of the elements together, separated with commas, and wrap
54 | // them in brackets.
55 | if (partial.length === 0) {
56 | v = '[]';
57 | } else if (innerIndent) {
58 | v = '[\n' +
59 | innerIndent +
60 | partial.join(',\n' +
61 | innerIndent) +
62 | '\n' +
63 | outerIndent +
64 | ']';
65 | } else {
66 | v = '[' + partial.join(',') + ']';
67 | }
68 | return v;
69 | }
70 |
71 | // Iterate through all of the keys in the object.
72 | let keys = Object.keys(value);
73 | if (canonical) {
74 | keys = keys.sort();
75 | }
76 | keys.forEach(k => {
77 | v = str(k, value, singleIndent, innerIndent, canonical);
78 | if (v) {
79 | partial.push(quote(k) + (innerIndent ? ': ' : ':') + v);
80 | }
81 | });
82 |
83 | // Join all of the member texts together, separated with commas,
84 | // and wrap them in braces.
85 | if (partial.length === 0) {
86 | v = '{}';
87 | } else if (innerIndent) {
88 | v = '{\n' +
89 | innerIndent +
90 | partial.join(',\n' +
91 | innerIndent) +
92 | '\n' +
93 | outerIndent +
94 | '}';
95 | } else {
96 | v = '{' + partial.join(',') + '}';
97 | }
98 | return v;
99 |
100 | default: // Do nothing
101 | }
102 | };
103 |
104 | // If the JSON object does not yet have a stringify method, give it one.
105 | const canonicalStringify = (value, options) => {
106 | // Make a fake root object containing our value under the key of ''.
107 | // Return the result of stringifying the value.
108 | const allOptions = Object.assign({
109 | indent: '',
110 | canonical: false,
111 | }, options);
112 | if (allOptions.indent === true) {
113 | allOptions.indent = ' ';
114 | } else if (typeof allOptions.indent === 'number') {
115 | let newIndent = '';
116 | for (let i = 0; i < allOptions.indent; i++) {
117 | newIndent += ' ';
118 | }
119 | allOptions.indent = newIndent;
120 | }
121 | return str('', {'': value}, allOptions.indent, '', allOptions.canonical);
122 | };
123 |
124 | export default canonicalStringify;
--------------------------------------------------------------------------------
/ddp-redux/src/ejson/tinytest.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-extraneous-dependencies: "off" */
2 | import chai from 'chai';
3 |
4 | const test = {
5 | isTrue: chai.assert.isTrue,
6 | isFalse: chai.assert.isFalse,
7 | equal: chai.assert.deepEqual,
8 | throws: chai.assert.throws,
9 | notEqual: chai.assert.notDeepEqual,
10 | };
11 |
12 | class Tinytest {
13 | constructor() {
14 | this.tests = [];
15 | }
16 |
17 | add(name, run) {
18 | this.tests.push({
19 | name,
20 | run,
21 | });
22 | }
23 |
24 | buildSuite(it) {
25 | this.tests.forEach(({ name, run }) => {
26 | it(name, () => run(test));
27 | });
28 | }
29 | }
30 |
31 | export default Tinytest;
32 |
--------------------------------------------------------------------------------
/ddp-redux/src/index.js:
--------------------------------------------------------------------------------
1 | import DDPClient from './DDPClient';
2 | import DDPError from './DDPError';
3 | import EJSON from './ejson';
4 | import { createCollectionSelectors } from './modules/collections/selectors';
5 | import { createCurrentUserSelectors } from './modules/currentUser/selectors';
6 | import { createSubscriptionsSelector } from './modules/subscriptions/selectors';
7 | import { createQueriesSelector } from './modules/queries/selectors';
8 | import { createConnectionSelector } from './modules/connection/selectors';
9 | import { createMethodsSelector } from './modules/methods/selectors';
10 | import './shim';
11 |
12 | export * from './actions';
13 | export * from './constants';
14 | export {
15 | EJSON,
16 | DDPError,
17 | createCollectionSelectors,
18 | createCurrentUserSelectors,
19 | createSubscriptionsSelector,
20 | createQueriesSelector,
21 | createConnectionSelector,
22 | createMethodsSelector,
23 | };
24 | export default DDPClient;
25 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/middleware.js:
--------------------------------------------------------------------------------
1 | import some from 'lodash/some';
2 | import {
3 | DDP_READY,
4 | DDP_UPDATED,
5 | DDP_METHOD,
6 | DDP_ADDED,
7 | DDP_CHANGED,
8 | DDP_REMOVED,
9 | DDP_FLUSH,
10 | DDP_QUERY_UPDATE,
11 | } from '../../constants';
12 |
13 | /**
14 | * Create middleware for the given ddpClient.
15 | * @param {DDPClient} ddpClient
16 | */
17 | export const createMiddleware = ddpClient => (store) => {
18 | let flushTimeout = null;
19 | const scheduleFlush = () => {
20 | if (flushTimeout) {
21 | clearTimeout(flushTimeout);
22 | }
23 | flushTimeout = setTimeout(() => {
24 | const state = store.getState();
25 | if (some(state.ddp.collections, collection => collection.needsUpdate)) {
26 | store.dispatch({
27 | type: DDP_FLUSH,
28 | });
29 | }
30 | flushTimeout = null;
31 | }, ddpClient.getFlushTimeout());
32 | };
33 | return next => (action) => {
34 | if (!action || typeof action !== 'object') {
35 | return next(action);
36 | }
37 | switch (action.type) {
38 | case DDP_ADDED:
39 | case DDP_CHANGED:
40 | case DDP_REMOVED:
41 | case DDP_METHOD:
42 | case DDP_QUERY_UPDATE:
43 | scheduleFlush();
44 | return next(action);
45 | case DDP_READY:
46 | case DDP_UPDATED:
47 | store.dispatch({
48 | type: DDP_FLUSH,
49 | });
50 | return next(action);
51 | default:
52 | return next(action);
53 | }
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/middleware.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | /* eslint no-invalid-this: "off" */
3 |
4 | import configureStore from 'redux-mock-store';
5 | import { createMiddleware } from './middleware';
6 | import {
7 | DDP_FLUSH,
8 | DDP_READY,
9 | DDP_UPDATED,
10 |
11 | DDP_ADDED,
12 | DDP_CHANGED,
13 | DDP_REMOVED,
14 | } from '../../constants';
15 | import { DDPClient } from './testCommon';
16 |
17 | jest.useFakeTimers();
18 |
19 | describe('Test module - collections - middleware', () => {
20 | let testContext;
21 |
22 | beforeEach(() => {
23 | testContext = {};
24 | });
25 |
26 | beforeEach(() => {
27 | testContext.send = jest.fn();
28 | testContext.ddpClient = new DDPClient();
29 | testContext.ddpClient.socket.send = testContext.send;
30 | testContext.middleware = createMiddleware(testContext.ddpClient);
31 | testContext.mockStore = configureStore([
32 | testContext.middleware,
33 | ]);
34 | });
35 |
36 | test('should pass through an unknown action', () => {
37 | const store = testContext.mockStore();
38 | const action = {
39 | type: 'unknown',
40 | payload: {},
41 | };
42 | store.dispatch(action);
43 | expect(store.getActions()).toEqual(expect.arrayContaining([
44 | action,
45 | ]));
46 | });
47 |
48 | [
49 | DDP_ADDED,
50 | DDP_CHANGED,
51 | DDP_REMOVED,
52 | ].forEach((type) => {
53 | test(`should schedule dispatching ${DDP_FLUSH} after ${type}`, () => {
54 | const store = testContext.mockStore({
55 | ddp: {
56 | collections: {
57 | col1: {
58 | needsUpdate: true,
59 | },
60 | },
61 | },
62 | });
63 | const action = {
64 | type,
65 | payload: {
66 | },
67 | meta: {
68 | socketId: 'socket/1',
69 | },
70 | };
71 | store.dispatch(action);
72 | expect(store.getActions()).toEqual([
73 | action,
74 | ]);
75 |
76 | jest.advanceTimersByTime(1000);
77 |
78 | expect(store.getActions()).toEqual([
79 | action,
80 | {
81 | type: DDP_FLUSH,
82 | },
83 | ]);
84 | });
85 | });
86 |
87 | [
88 | DDP_READY,
89 | DDP_UPDATED,
90 | ].forEach((type) => {
91 | test(`should dispatch ${DDP_FLUSH} right before ${type}`, () => {
92 | const store = testContext.mockStore({
93 | ddp: {
94 | collections: {
95 | col1: {
96 | needsUpdate: true,
97 | },
98 | },
99 | },
100 | });
101 | const action = {
102 | type,
103 | payload: {
104 | },
105 | };
106 | store.dispatch(action);
107 | expect(store.getActions()).toEqual([
108 | {
109 | type: DDP_FLUSH,
110 | },
111 | action,
112 | ]);
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/reducer.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import reduce from 'lodash/reduce';
3 | import isEmpty from 'lodash/isEmpty';
4 | import {
5 | DDP_METHOD,
6 | DDP_UPDATED,
7 | DDP_ADDED,
8 | DDP_ADDED_BEFORE,
9 | DDP_CHANGED,
10 | DDP_REMOVED,
11 | DDP_FLUSH,
12 | DDP_QUERY_UPDATE,
13 | DDP_QUERY_DELETE,
14 | } from '../../constants';
15 | import carefullyMapValues from '../../utils/carefullyMapValues';
16 | import createInsertEntities from '../../utils/createInsertEntities';
17 | import createRemoveEntities from '../../utils/createRemoveEntities';
18 |
19 | export const mutateCollections = (state, collection, id, socketId, mutateOne) => {
20 | const stateCollection = state[collection] || {};
21 | const stateCollectionById = stateCollection.nextById || {};
22 | const {
23 | current,
24 | ...other
25 | } = stateCollectionById[id] || {};
26 | const shouldRemove = !mutateOne;
27 | const newCurrent = shouldRemove
28 | ? omit(current, socketId)
29 | : {
30 | ...current,
31 | [socketId]: mutateOne(current && current[socketId]),
32 | };
33 | const shouldRemoveCurrent = isEmpty(newCurrent);
34 | const shouldRemoveCompletely = shouldRemoveCurrent && isEmpty(other);
35 | return {
36 | ...state,
37 | [collection]: {
38 | ...stateCollection,
39 | needsUpdate: true,
40 | nextById: shouldRemoveCompletely
41 | ? omit(stateCollectionById, id)
42 | : {
43 | ...stateCollectionById,
44 | [id]: shouldRemoveCurrent
45 | ? omit(stateCollectionById[id], 'current')
46 | : {
47 | ...stateCollectionById[id],
48 | current: newCurrent,
49 | },
50 | },
51 | },
52 | };
53 | };
54 |
55 | const insertQueries = createInsertEntities('queries', 'queriesOrder');
56 | const removeQueries = createRemoveEntities('queries', 'queriesOrder');
57 |
58 | const insertChanges = createInsertEntities('methods', 'methodsOrder');
59 | const removeChanges = createRemoveEntities('methods', 'methodsOrder');
60 |
61 | export const createReducer = () => (state = {}, action) => {
62 | switch (action.type) {
63 | case DDP_ADDED:
64 | case DDP_ADDED_BEFORE:
65 | return mutateCollections(
66 | state,
67 | action.payload.collection,
68 | action.payload.id,
69 | action.meta && action.meta.socketId,
70 | () => ({
71 | _id: action.payload.id,
72 | ...action.payload.fields,
73 | }),
74 | );
75 | case DDP_CHANGED:
76 | return mutateCollections(
77 | state,
78 | action.payload.collection,
79 | action.payload.id,
80 | action.meta && action.meta.socketId,
81 | doc => ({
82 | ...omit(doc, action.payload.cleared),
83 | ...action.payload.fields,
84 | }),
85 | );
86 | case DDP_REMOVED:
87 | return mutateCollections(
88 | state,
89 | action.payload.collection,
90 | action.payload.id,
91 | action.meta && action.meta.socketId,
92 | null,
93 | );
94 | case DDP_FLUSH:
95 | return carefullyMapValues(state, (collection) => {
96 | if (collection.needsUpdate) {
97 | const {
98 | needsUpdate,
99 | ...other
100 | } = collection;
101 | return {
102 | ...other,
103 | byId: collection.nextById,
104 | };
105 | }
106 | return collection;
107 | });
108 | case DDP_QUERY_UPDATE:
109 | return (() => {
110 | const { queryId } = action.meta;
111 | const {
112 | entities,
113 | oldEntities,
114 | } = action.payload;
115 | return insertQueries(
116 | removeQueries(
117 | state,
118 | queryId,
119 | oldEntities,
120 | ),
121 | queryId,
122 | entities,
123 | );
124 | })();
125 | case DDP_QUERY_DELETE:
126 | return (() => {
127 | const { queryId } = action.meta;
128 | const { entities } = action.payload;
129 | return removeQueries(
130 | state,
131 | queryId,
132 | entities,
133 | );
134 | })();
135 | case DDP_METHOD:
136 | return insertChanges(
137 | state,
138 | action.meta.methodId,
139 | action.meta.entities,
140 | );
141 | case DDP_UPDATED:
142 | return reduce(
143 | action.meta.methods,
144 | (previousState, method) => (method
145 | ? removeChanges(
146 | previousState,
147 | method.id,
148 | method.entities,
149 | )
150 | : previousState
151 | ),
152 | state,
153 | );
154 | default:
155 | return state;
156 | }
157 | };
158 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/selectors.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import { createSelectors } from './selectors';
4 | import { DDPClient } from './testCommon';
5 |
6 | const constant = x => () => x;
7 |
8 | describe('Test module - collections - selectors', () => {
9 | let testContext;
10 |
11 | beforeEach(() => {
12 | testContext = {};
13 | });
14 |
15 | beforeEach(() => {
16 | testContext.selectors = createSelectors(DDPClient);
17 | testContext.collections1 = {
18 | col1: {
19 | byId: {
20 | 1: {
21 | current: {
22 | 'socket/1': {
23 | _id: '1',
24 | a: 1,
25 | b: 2,
26 | },
27 | 'socket/2': {
28 | _id: '1',
29 | c: 3,
30 | },
31 | },
32 | queries: {
33 | 'query/1': {
34 | d: 4,
35 | },
36 | },
37 | queriesOrder: ['query/1'],
38 | },
39 | },
40 | },
41 | col2: {
42 | byId: {
43 | 1: {
44 | current: {
45 | 'socket/1': {
46 | _id: '1',
47 | a: 1,
48 | },
49 | },
50 | queries: {
51 | 'query/1': {
52 | _id: '1',
53 | a: 9,
54 | b: 2,
55 | },
56 | },
57 | queriesOrder: ['query/1'],
58 | },
59 | },
60 | },
61 | };
62 | testContext.collections2 = {
63 | ...testContext.collections1,
64 | col1: {
65 | ...testContext.collections1.col1,
66 | byId: {
67 | ...testContext.collections1.col1.byId,
68 | 2: {
69 | current: {
70 | 'socket/1': {
71 | _id: '2',
72 | a: 1,
73 | b: 2,
74 | },
75 | },
76 | },
77 | },
78 | },
79 | };
80 | testContext.collections3 = {
81 | ...testContext.collections2,
82 | col1: {
83 | ...testContext.collections2.col1,
84 | byId: {
85 | ...testContext.collections2.col1.byId,
86 | 2: {
87 | ...testContext.collections2.col1.byId[2],
88 | current: {
89 | ...testContext.collections2.col1.byId[2].current,
90 | 'socket/1': {
91 | ...testContext.collections2.col1.byId[2].current['socket/1'],
92 | a: 3,
93 | },
94 | },
95 | },
96 | },
97 | },
98 | };
99 | testContext.state1 = { ddp: { collections: testContext.collections1 } };
100 | testContext.state2 = { ddp: { collections: testContext.collections2 } };
101 | testContext.state3 = { ddp: { collections: testContext.collections3 } };
102 | });
103 |
104 | test('should select a document by id', () => {
105 | const selector = testContext.selectors.col1.one(constant('1'));
106 | const expected = {
107 | _id: '1',
108 | a: 1,
109 | b: 2,
110 | c: 3,
111 | d: 4,
112 | };
113 | const doc1 = selector(testContext.state1);
114 | const doc2 = selector(testContext.state2);
115 | expect(doc1).toEqual(expected);
116 | expect(doc2).toBe(doc1);
117 | expect(testContext.selectors.col1.all().byId().recomputations()).toBe(2);
118 | });
119 |
120 | test('should find all documents', () => {
121 | const predicate = constant(true);
122 | const selector = testContext.selectors.col1.where(() => predicate);
123 | const doc1 = {
124 | _id: '1',
125 | a: 1,
126 | b: 2,
127 | c: 3,
128 | d: 4,
129 | };
130 | const doc2 = {
131 | _id: '2',
132 | a: 1,
133 | b: 2,
134 | };
135 | const results1 = selector(testContext.state1);
136 | const results2 = selector(testContext.state2);
137 | expect(results1).toEqual([
138 | doc1,
139 | ]);
140 | expect(results2).toEqual([
141 | doc1,
142 | doc2,
143 | ]);
144 | });
145 |
146 | test('should find all matching documents', () => {
147 | const predicate = x => x.c === 3;
148 | const selector = testContext.selectors.col1.where(() => predicate);
149 | const doc1 = {
150 | _id: '1',
151 | a: 1,
152 | b: 2,
153 | c: 3,
154 | d: 4,
155 | };
156 | const results1 = selector(testContext.state1);
157 | const results2 = selector(testContext.state2);
158 | expect(results1).toEqual([
159 | doc1,
160 | ]);
161 | expect(results2).toEqual([
162 | doc1,
163 | ]);
164 | });
165 |
166 | test('should find one matching document', () => {
167 | const predicate = x => x.c === 3;
168 | const selector = testContext.selectors.col1.one.where(() => predicate);
169 | const doc1 = {
170 | _id: '1',
171 | a: 1,
172 | b: 2,
173 | c: 3,
174 | d: 4,
175 | };
176 | const results1 = selector(testContext.state1);
177 | const results2 = selector(testContext.state2);
178 | expect(results1).toEqual(doc1);
179 | expect(results2).toEqual(doc1);
180 | });
181 | });
182 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/collections/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 |
3 | export class DDPClient {
4 | constructor() {
5 | this.socket = new DDPEmitter();
6 | }
7 |
8 | getFlushTimeout() {
9 | return this.constructor.getFlushTimeout();
10 | }
11 |
12 | static getFlushTimeout() {
13 | return 200;
14 | }
15 | }
16 |
17 | export class Model1 {
18 | constructor(doc) {
19 | Object.assign(this, doc);
20 | }
21 | }
22 |
23 | Model1.indexes = {
24 | a: {},
25 | b: {},
26 | };
27 |
28 | export class Model2 {
29 | constructor(doc) {
30 | Object.assign(this, doc);
31 | }
32 | }
33 |
34 | Model2.indexes = {};
35 |
36 | DDPClient.models = {
37 | col1: Model1,
38 | col2: Model2,
39 | };
40 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/middleware.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import {
3 | DEFAULT_SOCKET_ID,
4 |
5 | DDP_PROTOCOL_VERSION,
6 | DDP_FAILED,
7 | DDP_ERROR,
8 | DDP_OPEN,
9 | DDP_CLOSE,
10 | DDP_DISCONNECTED,
11 | DDP_PING,
12 | DDP_PONG,
13 | DDP_CONNECT,
14 | } from '../../constants';
15 | import DDPError from '../../DDPError';
16 | import EJSON from '../../ejson';
17 | import createDelayedTask from '../../utils/createDelayedTask';
18 |
19 | /**
20 | * Create middleware for the given ddpClient.
21 | * @param {DDPClient} ddpClient
22 | */
23 | export const createMiddleware = ddpClient => (store) => {
24 | // TODO: Add support for "server_id" message.
25 | ddpClient.on('open', (meta) => {
26 | store.dispatch({
27 | meta,
28 | type: DDP_CONNECT,
29 | payload: {
30 | version: DDP_PROTOCOL_VERSION,
31 | support: [DDP_PROTOCOL_VERSION],
32 | },
33 | });
34 | });
35 | ddpClient.on('close', (meta) => {
36 | store.dispatch({
37 | meta,
38 | type: DDP_DISCONNECTED,
39 | });
40 | });
41 | if (ddpClient.defaultEndpoint) {
42 | setTimeout(() => {
43 | store.dispatch({
44 | type: DDP_OPEN,
45 | payload: {
46 | endpoint: ddpClient.defaultEndpoint,
47 | },
48 | });
49 | });
50 | }
51 | const scheduleCleanup = createDelayedTask((socketId) => {
52 | ddpClient.close({ socketId });
53 | });
54 | return next => (action) => {
55 | if (!action || typeof action !== 'object') {
56 | return next(action);
57 | }
58 | switch (action.type) {
59 | case DDP_ERROR:
60 | ddpClient.emit('error', new DDPError(DDPError.ERROR_BAD_MESSAGE, action.payload.reason, action.payload.offendingMessage));
61 | return next(action);
62 | case DDP_PING:
63 | return ((result) => {
64 | store.dispatch({
65 | type: DDP_PONG,
66 | payload: {
67 | id: action.payload.id,
68 | },
69 | meta: action.meta,
70 | });
71 | return result;
72 | })(next(action));
73 | case DDP_FAILED: // could not negotiate DDP protocol version
74 | ddpClient.close(action.meta);
75 | // NOTE: ddpClient.close() will emit "close" event and it will dispatch the DDP_DISCONNECTED
76 | return next(action);
77 | case DDP_OPEN:
78 | return (() => {
79 | const state = store.getState();
80 | const {
81 | endpoint,
82 | params,
83 | } = action.payload;
84 | const socket = find(state.ddp.connection.sockets, x => x.endpoint === endpoint && EJSON.equals(x.params, params));
85 | let socketId = socket && socket.id;
86 | if (!socketId) {
87 | if (!state.ddp.connection.sockets[DEFAULT_SOCKET_ID]) {
88 | socketId = DEFAULT_SOCKET_ID;
89 | } else {
90 | socketId = ddpClient.nextUniqueId();
91 | }
92 | }
93 | if (socket) {
94 | scheduleCleanup.cancel(socketId);
95 | } else {
96 | ddpClient.open(endpoint, { socketId });
97 | }
98 | next({
99 | ...action,
100 | payload: {
101 | ...action.payload,
102 | },
103 | meta: {
104 | ...action.meta,
105 | socketId,
106 | },
107 | });
108 | return socketId;
109 | })();
110 | case DDP_CLOSE:
111 | return (() => {
112 | const state = store.getState();
113 | const socket = state.ddp.connection.sockets[action.meta.socketId];
114 | // NOTE: The number of users will only be decreased after "next(action)"
115 | // so at this moment it's still taking into account the one which
116 | // is resigning.
117 | if (socket && socket.users === 1) {
118 | scheduleCleanup(socket.id);
119 | }
120 | return next(action);
121 | })();
122 | default:
123 | return next(action);
124 | }
125 | };
126 | };
127 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_SOCKET_ID,
3 |
4 | DDP_CONNECTION_STATE__CONNECTING,
5 | DDP_CONNECTION_STATE__CONNECTED,
6 | DDP_CONNECTION_STATE__DISCONNECTED,
7 |
8 | DDP_OPEN,
9 | DDP_CLOSE,
10 | DDP_DISCONNECTED,
11 | DDP_CONNECTED,
12 | DDP_CONNECT,
13 | } from '../../constants';
14 | import carefullyMapValues from '../../utils/carefullyMapValues';
15 |
16 | export const createSocketReducer = () => (state = {
17 | state: DDP_CONNECTION_STATE__DISCONNECTED,
18 | }, action) => {
19 | switch (action.type) {
20 | case DDP_CONNECT:
21 | return {
22 | ...state,
23 | state: DDP_CONNECTION_STATE__CONNECTING,
24 | };
25 | case DDP_CONNECTED:
26 | return {
27 | ...state,
28 | state: DDP_CONNECTION_STATE__CONNECTED,
29 | };
30 | case DDP_DISCONNECTED:
31 | return {
32 | ...state,
33 | state: DDP_CONNECTION_STATE__DISCONNECTED,
34 | };
35 | default:
36 | return state;
37 | }
38 | };
39 |
40 | export const createReducer = (DDPClient) => {
41 | const socketReducer = createSocketReducer(DDPClient);
42 | return (state = {
43 | sockets: {},
44 | }, action) => {
45 | switch (action.type) {
46 | case DDP_OPEN:
47 | return (() => {
48 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
49 | return {
50 | ...state,
51 | sockets: {
52 | ...state.sockets,
53 | [socketId]: {
54 | ...state.sockets[socketId],
55 | id: socketId,
56 | users: ((state.sockets[socketId] && state.sockets[socketId].users) || 0) + 1,
57 | params: action.payload.params,
58 | endpoint: action.payload.endpoint,
59 | },
60 | },
61 | };
62 | })();
63 | case DDP_CLOSE:
64 | return (() => {
65 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
66 | return {
67 | ...state,
68 | sockets: {
69 | ...state.sockets,
70 | [socketId]: {
71 | ...state.sockets[socketId],
72 | users: ((state.sockets[socketId] && state.sockets[socketId].users) || 0) - 1,
73 | },
74 | },
75 | };
76 | })();
77 | case DDP_CONNECT:
78 | case DDP_CONNECTED:
79 | case DDP_DISCONNECTED:
80 | return (() => {
81 | const actionSocketId = action.meta && action.meta.socketId;
82 | // NOTE: If socket is not yet there, it will be created.
83 | if (actionSocketId) {
84 | return {
85 | ...state,
86 | sockets: carefullyMapValues(state.sockets, (socket, socketId, remove) => {
87 | if (actionSocketId === socketId) {
88 | if (action.type === DDP_DISCONNECTED && !socket.users) {
89 | return remove(socketId);
90 | }
91 | return socketReducer(socket, action);
92 | }
93 | return socket;
94 | }),
95 | };
96 | }
97 | return state;
98 | })();
99 | default:
100 | return state;
101 | }
102 | };
103 | };
104 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/reducer.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import { createReducer } from './reducer';
4 | import {
5 | DDP_CONNECTION_STATE__DISCONNECTED,
6 | DDP_CONNECTION_STATE__CONNECTING,
7 | DDP_CONNECTION_STATE__CONNECTED,
8 |
9 | DDP_CONNECT,
10 | DDP_CONNECTED,
11 | DDP_DISCONNECTED,
12 | DDP_OPEN,
13 | DDP_CLOSE,
14 | } from '../../constants';
15 | import { DDPClient } from './testCommon';
16 |
17 | describe('Test module - connection - reducer', () => {
18 | let testContext;
19 |
20 | beforeEach(() => {
21 | testContext = {};
22 | });
23 |
24 | beforeEach(() => {
25 | testContext.reducer = createReducer(DDPClient);
26 | });
27 |
28 | test('should initialize state', () => {
29 | expect(testContext.reducer(undefined, {})).toEqual({
30 | sockets: {},
31 | });
32 | });
33 |
34 | test('should change state to "connecting"', () => {
35 | expect(testContext.reducer({
36 | sockets: {
37 | 1: {
38 | state: DDP_CONNECTION_STATE__DISCONNECTED,
39 | },
40 | },
41 | }, {
42 | type: DDP_CONNECT,
43 | payload: {},
44 | meta: {
45 | socketId: '1',
46 | },
47 | })).toEqual({
48 | sockets: {
49 | 1: {
50 | state: DDP_CONNECTION_STATE__CONNECTING,
51 | },
52 | },
53 | });
54 | });
55 |
56 | test('should change state to "connected"', () => {
57 | expect(testContext.reducer({
58 | sockets: {
59 | 1: {
60 | state: DDP_CONNECTION_STATE__CONNECTING,
61 | },
62 | },
63 | }, {
64 | type: DDP_CONNECTED,
65 | payload: {},
66 | meta: {
67 | socketId: '1',
68 | },
69 | })).toEqual({
70 | sockets: {
71 | 1: {
72 | state: DDP_CONNECTION_STATE__CONNECTED,
73 | },
74 | },
75 | });
76 | });
77 |
78 | test('should change state to "disconnected"', () => {
79 | expect(testContext.reducer({
80 | sockets: {
81 | 1: {
82 | state: DDP_CONNECTION_STATE__CONNECTED,
83 | users: 1,
84 | },
85 | },
86 | }, {
87 | type: DDP_DISCONNECTED,
88 | payload: {},
89 | meta: {
90 | socketId: '1',
91 | },
92 | })).toEqual({
93 | sockets: {
94 | 1: {
95 | state: DDP_CONNECTION_STATE__DISCONNECTED,
96 | users: 1,
97 | },
98 | },
99 | });
100 | });
101 |
102 | test('should remove socket completely if there are no users', () => {
103 | expect(testContext.reducer({
104 | sockets: {
105 | 1: {
106 | state: DDP_CONNECTION_STATE__CONNECTED,
107 | users: 0,
108 | },
109 | 2: {
110 | state: DDP_CONNECTION_STATE__CONNECTED,
111 | users: 1,
112 | },
113 | },
114 | }, {
115 | type: DDP_DISCONNECTED,
116 | payload: {},
117 | meta: {
118 | socketId: '1',
119 | },
120 | })).toEqual({
121 | sockets: {
122 | 2: {
123 | state: DDP_CONNECTION_STATE__CONNECTED,
124 | users: 1,
125 | },
126 | },
127 | });
128 | });
129 |
130 | test('should increase the number of users', () => {
131 | expect(testContext.reducer({
132 | sockets: {
133 | 1: {
134 | id: '1',
135 | endpoint: 'http://example.com',
136 | params: [],
137 | state: DDP_CONNECTION_STATE__CONNECTED,
138 | users: 1,
139 | },
140 | },
141 | }, {
142 | type: DDP_OPEN,
143 | payload: {
144 | endpoint: 'http://example.com',
145 | params: [],
146 | },
147 | meta: {
148 | socketId: '1',
149 | },
150 | })).toEqual({
151 | sockets: {
152 | 1: {
153 | id: '1',
154 | endpoint: 'http://example.com',
155 | params: [],
156 | state: DDP_CONNECTION_STATE__CONNECTED,
157 | users: 2,
158 | },
159 | },
160 | });
161 | });
162 |
163 | test('should decrease the number of users', () => {
164 | expect(testContext.reducer({
165 | sockets: {
166 | 1: {
167 | state: DDP_CONNECTION_STATE__CONNECTED,
168 | users: 1,
169 | },
170 | },
171 | }, {
172 | type: DDP_CLOSE,
173 | payload: {
174 | },
175 | meta: {
176 | socketId: '1',
177 | },
178 | })).toEqual({
179 | sockets: {
180 | 1: {
181 | state: DDP_CONNECTION_STATE__CONNECTED,
182 | users: 0,
183 | },
184 | },
185 | });
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/selectors.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import { createSelector } from 'reselect';
3 | import EJSON from '../../ejson';
4 | import { DEFAULT_SOCKET_ID } from '../../constants';
5 |
6 | export const createConnectionSelector = ({
7 | selectDeclaredConnection,
8 | }) => createSelector(
9 | selectDeclaredConnection,
10 | state => state.ddp && state.ddp.connection && state.ddp.connection.sockets,
11 | (y, state) => (y
12 | ? find(
13 | state,
14 | x => x.endpoint === y.endpoint && EJSON.equals(x.params, y.params),
15 | )
16 | : state[DEFAULT_SOCKET_ID]
17 | ),
18 | );
19 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/connection/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 | import { DEFAULT_SOCKET_ID } from '../../constants';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | constructor() {
6 | super();
7 | this.sockets = {};
8 | }
9 |
10 | nextUniqueId() {
11 | return this.constructor.defaultUniqueId;
12 | }
13 |
14 | open(endpoint, { socketId = DEFAULT_SOCKET_ID } = {}) {
15 | this.sockets[socketId] = {
16 | endpoint,
17 | };
18 | }
19 |
20 | close({ socketId = DEFAULT_SOCKET_ID } = {}) {
21 | delete this.sockets[socketId];
22 | }
23 | }
24 |
25 | DDPClient.defaultUniqueId = '1';
26 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/middleware.js:
--------------------------------------------------------------------------------
1 | /** @module createUser/middleware */
2 |
3 | import {
4 | DEFAULT_SOCKET_ID,
5 | DEFAULT_LOGIN_METHOD_NAME,
6 | DEFAULT_LOGOUT_METHOD_NAME,
7 |
8 | DDP_LOGIN,
9 | DDP_LOGGED_IN,
10 | DDP_LOGOUT,
11 | DDP_LOGGED_OUT,
12 | DDP_CONNECTED,
13 |
14 | LOGIN_ACTION_PRIORITY,
15 | } from '../../constants';
16 | import { callMethod } from '../../actions';
17 |
18 | /**
19 | * Create middleware for the given ddpClient.
20 | * @param {DDPClient} ddpClient
21 | * @returns {ReduxMiddleware}
22 | * @private
23 | */
24 | export const createMiddleware = ddpClient => (store) => {
25 | const getSocket = (socketId) => {
26 | const state = store.getState();
27 | return state.ddp &&
28 | state.ddp.connection &&
29 | state.ddp.connection.sockets &&
30 | state.ddp.connection.sockets[socketId];
31 | };
32 | const handleLoginError = (meta, err) => {
33 | store.dispatch({
34 | type: DDP_LOGGED_OUT,
35 | error: true,
36 | payload: err,
37 | meta,
38 | });
39 | ddpClient.emit('error', err);
40 | };
41 | return next => (action) => {
42 | if (!action || typeof action !== 'object') {
43 | return next(action);
44 | }
45 | switch (action.type) {
46 | case DDP_CONNECTED:
47 | return ((result) => {
48 | const { socketId } = action.meta;
49 | const socket = getSocket(socketId);
50 | ddpClient
51 | .getResumeToken(socket)
52 | .then(resume => resume && store.dispatch({
53 | type: DDP_LOGIN,
54 | payload: [{ resume }],
55 | meta: {
56 | socketId,
57 | },
58 | }))
59 | .catch((err) => {
60 | console.warn(err);
61 | });
62 | return result;
63 | })(next(action));
64 | case DDP_LOGGED_IN:
65 | return (() => {
66 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
67 | const socket = getSocket(socketId);
68 | ddpClient.setResumeToken(socket, action.payload.token);
69 | return next(action);
70 | })();
71 | case DDP_LOGGED_OUT:
72 | return (() => {
73 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
74 | const socket = getSocket(socketId);
75 | ddpClient.clearResumeToken(socket);
76 | return next(action);
77 | })();
78 | case DDP_LOGIN:
79 | return (() => {
80 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
81 | const method = (action.meta && action.meta.method) || DEFAULT_LOGIN_METHOD_NAME;
82 | next(action);
83 | const result = store.dispatch(callMethod(method, action.payload, {
84 | ...action.meta,
85 | priority: LOGIN_ACTION_PRIORITY,
86 | }));
87 | if (result instanceof Promise) {
88 | result.then(({ id, token }) => store.dispatch({
89 | type: DDP_LOGGED_IN,
90 | payload: {
91 | id,
92 | token,
93 | },
94 | meta: {
95 | socketId,
96 | },
97 | }), handleLoginError.bind(null, action.meta));
98 | }
99 | return result;
100 | })();
101 | case DDP_LOGOUT:
102 | return (() => {
103 | next(action);
104 | const result = store.dispatch(callMethod(DEFAULT_LOGOUT_METHOD_NAME, [], {
105 | ...action.meta,
106 | priority: LOGIN_ACTION_PRIORITY,
107 | }));
108 | if (result instanceof Promise) {
109 | result
110 | .then(() => store.dispatch({
111 | type: DDP_LOGGED_OUT,
112 | meta: action.meta,
113 | }), handleLoginError.bind(null, action.meta));
114 | }
115 | return result;
116 | })();
117 | default:
118 | return next(action);
119 | }
120 | };
121 | };
122 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/middleware.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import configureStore from 'redux-mock-store';
4 | import { createMiddleware } from './middleware';
5 | import { DDPClient } from './testCommon';
6 |
7 | jest.useFakeTimers();
8 |
9 | describe('Test module - currentUser - middleware', () => {
10 | let testContext;
11 |
12 | beforeEach(() => {
13 | testContext = {};
14 | });
15 |
16 | beforeEach(() => {
17 | testContext.send = jest.fn();
18 | testContext.close = jest.fn();
19 | testContext.onError = jest.fn();
20 | testContext.ddpClient = new DDPClient();
21 | testContext.ddpClient.send = testContext.send;
22 | testContext.ddpClient.close = testContext.close;
23 | testContext.ddpClient.on('error', testContext.onError);
24 | testContext.middleware = createMiddleware(testContext.ddpClient);
25 | testContext.mockStore = configureStore([
26 | testContext.middleware,
27 | ]);
28 | });
29 |
30 | test('should pass through an unknown action', () => {
31 | const store = testContext.mockStore();
32 | const action = {
33 | type: 'unknown',
34 | payload: {},
35 | };
36 | store.dispatch(action);
37 | expect(store.getActions()).toEqual(expect.arrayContaining([
38 | action,
39 | ]));
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/reducer.js:
--------------------------------------------------------------------------------
1 | /** @module createUser/reducer */
2 |
3 | import omit from 'lodash/omit';
4 | import {
5 | DDP_LOGOUT,
6 | DDP_LOGGED_OUT,
7 | DDP_LOGIN,
8 | DDP_LOGGED_IN,
9 | DDP_DISCONNECTED,
10 |
11 | DDP_USER_STATE__LOGGING_IN,
12 | DDP_USER_STATE__LOGGED_IN,
13 | } from '../../constants';
14 |
15 | export const createSocketReducer = () => (state = {}, action) => {
16 | switch (action.type) {
17 | case DDP_LOGIN:
18 | case DDP_LOGOUT:
19 | return {
20 | ...state,
21 | state: DDP_USER_STATE__LOGGING_IN,
22 | };
23 | case DDP_LOGGED_IN:
24 | return {
25 | ...state,
26 | state: DDP_USER_STATE__LOGGED_IN,
27 | userId: action.payload.id,
28 | };
29 | default:
30 | return state;
31 | }
32 | };
33 |
34 | export const createReducer = (DDPClient) => {
35 | const socketReducer = createSocketReducer(DDPClient);
36 | return (state = {}, action) => {
37 | switch (action.type) {
38 | case DDP_LOGGED_OUT:
39 | case DDP_DISCONNECTED:
40 | return (() => {
41 | const socketId = action.meta && action.meta.socketId;
42 | if (socketId && state[socketId]) {
43 | return omit(state, socketId);
44 | }
45 | return state;
46 | })();
47 | case DDP_LOGIN:
48 | case DDP_LOGOUT:
49 | case DDP_LOGGED_IN:
50 | return (() => {
51 | if (!action.meta) {
52 | return state;
53 | }
54 | const {
55 | socketId,
56 | } = action.meta;
57 | // NOTE: If socket is not yet there, it will be created.
58 | if (socketId) {
59 | return {
60 | ...state,
61 | [socketId]: socketReducer(state[socketId], action),
62 | };
63 | }
64 | return state;
65 | })();
66 | default:
67 | return state;
68 | }
69 | };
70 | };
71 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/reducer.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import { createReducer } from './reducer';
4 | import {
5 | DDP_USER_STATE__LOGGING_IN,
6 | DDP_USER_STATE__LOGGED_IN,
7 |
8 | DDP_LOGIN,
9 | DDP_LOGGED_IN,
10 | DDP_LOGOUT,
11 | DDP_LOGGED_OUT,
12 | DDP_DISCONNECTED,
13 | } from '../../constants';
14 | import { DDPClient } from './testCommon';
15 |
16 | describe('Test module - currentUser - reducer', () => {
17 | let testContext;
18 |
19 | beforeEach(() => {
20 | testContext = {};
21 | });
22 |
23 | beforeEach(() => {
24 | testContext.reducer = createReducer(DDPClient);
25 | });
26 |
27 | test('should initialize state', () => {
28 | expect(testContext.reducer(undefined, {})).toEqual({});
29 | });
30 |
31 | test('should create a new entry on login', () => {
32 | expect(testContext.reducer({}, {
33 | type: DDP_LOGIN,
34 | payload: {
35 | },
36 | meta: {
37 | socketId: 'socket/1',
38 | },
39 | })).toEqual({
40 | 'socket/1': {
41 | state: DDP_USER_STATE__LOGGING_IN,
42 | },
43 | });
44 | });
45 |
46 | test('should set state to "logginIn" on logout', () => {
47 | expect(testContext.reducer({}, {
48 | type: DDP_LOGOUT,
49 | meta: {
50 | socketId: 'socket/1',
51 | },
52 | })).toEqual({
53 | 'socket/1': {
54 | state: DDP_USER_STATE__LOGGING_IN,
55 | },
56 | });
57 | });
58 |
59 | test('should set user id on logged in', () => {
60 | expect(testContext.reducer({}, {
61 | type: DDP_LOGGED_IN,
62 | payload: {
63 | id: '1234',
64 | },
65 | meta: {
66 | socketId: 'socket/1',
67 | },
68 | })).toEqual({
69 | 'socket/1': {
70 | state: DDP_USER_STATE__LOGGED_IN,
71 | userId: '1234',
72 | },
73 | });
74 | });
75 |
76 | test('should remove an entry on logged out', () => {
77 | expect(testContext.reducer({
78 | 'socket/1': {
79 | userId: '1234',
80 | },
81 | 'socker/2': {
82 | userId: '1234',
83 | },
84 | }, {
85 | type: DDP_LOGGED_OUT,
86 | payload: {
87 | },
88 | meta: {
89 | socketId: 'socket/1',
90 | },
91 | })).toEqual({
92 | 'socker/2': {
93 | userId: '1234',
94 | },
95 | });
96 | });
97 |
98 | test('should remove an entry on disconnected', () => {
99 | expect(testContext.reducer({
100 | 'socket/1': {
101 | userId: '1234',
102 | },
103 | 'socker/2': {
104 | userId: '1234',
105 | },
106 | }, {
107 | type: DDP_DISCONNECTED,
108 | payload: {
109 | },
110 | meta: {
111 | socketId: 'socket/1',
112 | },
113 | })).toEqual({
114 | 'socker/2': {
115 | userId: '1234',
116 | },
117 | });
118 | });
119 | });
120 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import {
3 | DEFAULT_SOCKET_ID,
4 | DDP_USER_STATE__LOGGING_IN,
5 | } from '../../constants';
6 | import { createCollectionSelectors } from '../collections/selectors';
7 |
8 | const constant = x => () => x;
9 | const identity = x => x;
10 |
11 | const noModelSpecified = () => {
12 | console.warn(`You attempted to "selectCurrent" user but no User model was specified.
13 | This will result with null value being returned even if a user is logged in. To fix this,
14 | please make sure that you pass a valid Model and collection name to createCollectionSelectors().`);
15 | };
16 |
17 | export const createCurrentUserSelectors = (Model, collection, {
18 | selectConnectionId = constant(DEFAULT_SOCKET_ID),
19 | } = {}) => {
20 | const usersCollection = collection || (Model && Model.collection);
21 | const userSelectorCreators = usersCollection
22 | ? createCollectionSelectors(Model, usersCollection)
23 | : null;
24 |
25 | const selectCurrentUserState = createSelector(
26 | selectConnectionId,
27 | identity,
28 | (connectionId, state) => (connectionId
29 | ? state.ddp &&
30 | state.ddp.currentUser[connectionId]
31 | : null
32 | ),
33 | );
34 |
35 | const selectCurrentUserId = createSelector(
36 | selectCurrentUserState,
37 | state => state && state.userId,
38 | );
39 |
40 | const selectCurrent = userSelectorCreators
41 | ? userSelectorCreators.one(selectCurrentUserId)
42 | : noModelSpecified;
43 |
44 | const selectIsLoggingIn = createSelector(
45 | selectCurrentUserState,
46 | state => !!(state && state.state === DDP_USER_STATE__LOGGING_IN),
47 | );
48 |
49 | // Example usage would be:
50 | //
51 | // current(User).user()
52 | // current(User).userId()
53 | // current(User).isLoggingIn()
54 |
55 | return {
56 | user: constant(selectCurrent),
57 | userId: constant(selectCurrentUserId),
58 | isLoggingIn: constant(selectIsLoggingIn),
59 | };
60 | };
61 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/currentUser/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 | import { DEFAULT_SOCKET_ID } from '../../constants';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | constructor() {
6 | super();
7 | this.sockets = {};
8 | }
9 |
10 | nextUniqueId() {
11 | return this.constructor.defaultUniqueId;
12 | }
13 |
14 | open(endpoint, { socketId = DEFAULT_SOCKET_ID } = {}) {
15 | this.sockets[socketId] = {
16 | endpoint,
17 | };
18 | }
19 |
20 | close({ socketId = DEFAULT_SOCKET_ID } = {}) {
21 | delete this.sockets[socketId];
22 | }
23 | }
24 |
25 | DDPClient.defaultUniqueId = '1';
26 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/messages/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/messages/middleware.js:
--------------------------------------------------------------------------------
1 | import max from 'lodash/max';
2 | import values from 'lodash/values';
3 | import {
4 | DEFAULT_SOCKET_ID,
5 |
6 | DDP_RESULT,
7 | DDP_CONNECTED,
8 | DDP_METHOD,
9 | DDP_SUB,
10 | DDP_ENQUEUE,
11 |
12 | MESSAGE_TO_ACTION,
13 | ACTION_TO_MESSAGE,
14 | ACTION_TO_PRIORITY,
15 | } from '../../constants';
16 |
17 | /**
18 | * Return the maximal priority of the current pending messages.
19 | * @param {object} state
20 | * @param {string} socketId
21 | * @returns {number}
22 | */
23 | const getMessageThreshold = (state, socketId) => {
24 | const priorities = values(state.ddp.messages.sockets[socketId] &&
25 | state.ddp.messages.sockets[socketId].pending);
26 | if (priorities.length === 0) {
27 | return -Infinity;
28 | }
29 | return max(priorities);
30 | };
31 |
32 | /**
33 | * Create middleware for the given ddpClient.
34 | * @param {DDPClient} ddpClient
35 | */
36 | export const createMiddleware = ddpClient => (store) => {
37 | ddpClient.on('message', (payload, meta) => {
38 | const type = payload.msg && MESSAGE_TO_ACTION[payload.msg];
39 | if (type) {
40 | store.dispatch({
41 | type,
42 | payload,
43 | meta,
44 | });
45 | }
46 | });
47 | return next => (action) => {
48 | if (!action || typeof action !== 'object') {
49 | return next(action);
50 | }
51 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
52 | if (action.type === DDP_CONNECTED || action.type === DDP_RESULT) {
53 | // NOTE: We are propagating action first, because
54 | // we want to get an up-to-date threshold.
55 | const result = next(action);
56 | const state = store.getState();
57 | const queue = state.ddp.messages.sockets[socketId] &&
58 | state.ddp.messages.sockets[socketId].queue;
59 | if (queue) {
60 | let t = getMessageThreshold(state, socketId);
61 | let i = 0;
62 | while (i < queue.length && t <= queue[i].meta.priority) {
63 | store.dispatch(queue[i]);
64 | // Note that threshold might have changed after dispatching another action.
65 | t = getMessageThreshold(store.getState(), socketId);
66 | i += 1;
67 | }
68 | }
69 | return result;
70 | }
71 | const msg = ACTION_TO_MESSAGE[action.type];
72 | if (!msg) {
73 | return next(action);
74 | }
75 | const priority = ACTION_TO_PRIORITY[action.type] || 0;
76 | const newAction = {
77 | ...action,
78 | payload: {
79 | ...action.payload,
80 | msg,
81 | },
82 | meta: {
83 | priority, // action may overwrite it's priority
84 | socketId, // action may overwrite it's socketId
85 | ...action.meta,
86 | },
87 | };
88 | // Ensure that method & sub messages always have valid unique id
89 | if (action.type === DDP_METHOD || action.type === DDP_SUB) {
90 | newAction.payload.id = newAction.payload.id || ddpClient.nextUniqueId();
91 | }
92 | const state = store.getState();
93 | const threshold = getMessageThreshold(state, socketId);
94 | if (newAction.meta.priority >= threshold) {
95 | // NOTE: Initially (before "connected" message is received), the threshold will be set
96 | // to "connect" action priority which is the highest possible. As a result, nothing
97 | // will be sent until the connection is established.
98 | ddpClient.send(newAction.payload, newAction.meta);
99 | return next(newAction);
100 | }
101 | return store.dispatch({
102 | type: DDP_ENQUEUE,
103 | payload: newAction.payload,
104 | meta: {
105 | type: newAction.type,
106 | ...newAction.meta,
107 | },
108 | });
109 | };
110 | };
111 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/messages/reducer.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import {
3 | DEFAULT_SOCKET_ID,
4 |
5 | DDP_DISCONNECTED,
6 | DDP_PONG,
7 | DDP_RESULT,
8 | DDP_CONNECTED,
9 | DDP_CONNECT,
10 | DDP_METHOD,
11 | DDP_SUB,
12 | DDP_UNSUB,
13 | DDP_ENQUEUE,
14 | DDP_OPEN,
15 |
16 | ACTION_TO_PRIORITY,
17 | } from '../../constants';
18 | import carefullyMapValues from '../../utils/carefullyMapValues';
19 |
20 | const initialPending = {
21 | '[connect]': ACTION_TO_PRIORITY[DDP_CONNECT],
22 | };
23 |
24 | export const createSocketReducer = () => (state = {
25 | queue: [],
26 | pending: initialPending,
27 | }, action) => {
28 | switch (action.type) {
29 | case DDP_DISCONNECTED:
30 | return {
31 | ...state,
32 | pending: initialPending,
33 | };
34 | case DDP_ENQUEUE:
35 | return {
36 | ...state,
37 | queue: ((queue) => {
38 | // elements with higher priority go first
39 | const priority = action.meta.priority || 0;
40 | const newQueue = [];
41 | let i = 0;
42 | while (i < queue.length && priority <= queue[i].meta.priority) {
43 | newQueue.push(queue[i]);
44 | i += 1;
45 | }
46 | const {
47 | type,
48 | ...meta
49 | } = action.meta;
50 | newQueue.push({
51 | type,
52 | meta,
53 | payload: action.payload,
54 | });
55 | while (i < queue.length) {
56 | newQueue.push(queue[i]);
57 | i += 1;
58 | }
59 | return newQueue;
60 | })(state.queue),
61 | };
62 | case DDP_CONNECTED:
63 | return {
64 | ...state,
65 | pending: {},
66 | };
67 | case DDP_METHOD:
68 | return {
69 | ...state,
70 | queue: state.queue.filter(x => x.payload.id !== action.payload.id),
71 | pending: {
72 | ...state.pending,
73 | [action.payload.id]: action.meta.priority,
74 | },
75 | };
76 | case DDP_RESULT:
77 | return {
78 | ...state,
79 | pending: omit(state.pending, action.payload.id),
80 | };
81 | case DDP_PONG:
82 | case DDP_SUB:
83 | case DDP_UNSUB:
84 | return {
85 | ...state,
86 | queue: state.queue.filter(x => x.payload.id !== action.payload.id),
87 | };
88 | default:
89 | return state;
90 | }
91 | };
92 |
93 | export const createReducer = (DDPClient) => {
94 | const socketReducer = createSocketReducer(DDPClient);
95 | return (state = {
96 | sockets: {},
97 | }, action) => {
98 | switch (action.type) {
99 | case DDP_OPEN:
100 | return (() => {
101 | const socketId = (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID;
102 | if (socketId) {
103 | return {
104 | ...state,
105 | sockets: {
106 | ...state.sockets,
107 | [socketId]: socketReducer(undefined, action),
108 | },
109 | };
110 | }
111 | return state;
112 | })();
113 | case DDP_DISCONNECTED:
114 | case DDP_ENQUEUE:
115 | case DDP_CONNECTED:
116 | case DDP_METHOD:
117 | case DDP_RESULT:
118 | case DDP_PONG:
119 | case DDP_SUB:
120 | case DDP_UNSUB:
121 | return (() => {
122 | if (action.meta && action.meta.socketId) {
123 | return {
124 | ...state,
125 | sockets: carefullyMapValues(state.sockets, (socket, socketId) => {
126 | if (socketId === action.meta.socketId) {
127 | return socketReducer(socket, action);
128 | }
129 | return socket;
130 | }),
131 | };
132 | }
133 | return state;
134 | })();
135 | default:
136 | return state;
137 | }
138 | };
139 | };
140 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/messages/selectors.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apendua/ddp-redux/412b4e38de09d398ad7bcc964f920f6fcf907396/ddp-redux/src/modules/messages/selectors.js
--------------------------------------------------------------------------------
/ddp-redux/src/modules/messages/testCommon.js:
--------------------------------------------------------------------------------
1 | /* eslint class-methods-use-this: "off" */
2 | import DDPEmitter from '../../DDPEmitter';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | nextUniqueId() {
6 | return '1';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/helpers.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Pick fields from metadata object that can potentially
4 | * be stored directly at the method object in redux store,
5 | * and vice versa.
6 | * @param {object} object
7 | * @returns {object}
8 | */
9 | export const extractMetadata = (object) => {
10 | if (!object) {
11 | return {};
12 | }
13 | const {
14 | queryId,
15 | socketId,
16 | entities,
17 | } = object;
18 | const result = {};
19 | if (queryId) {
20 | result.queryId = queryId;
21 | }
22 | if (socketId) {
23 | result.socketId = socketId;
24 | }
25 | if (entities) {
26 | result.entities = entities;
27 | }
28 | return result;
29 | };
30 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/reducer.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import {
3 | DDP_METHOD_STATE__QUEUED,
4 | DDP_METHOD_STATE__PENDING,
5 | DDP_METHOD_STATE__UPDATED,
6 | DDP_METHOD_STATE__RETURNED,
7 |
8 | DDP_CANCEL,
9 | DDP_METHOD,
10 | DDP_RESULT,
11 | DDP_UPDATED,
12 | DDP_ENQUEUE,
13 | } from '../../constants';
14 | import { extractMetadata } from './helpers';
15 | import carefullyMapValues from '../../utils/carefullyMapValues';
16 |
17 | export const createReducer = () => (state = {}, action) => {
18 | const id = action.meta && action.meta.methodId;
19 | switch (action.type) {
20 | case DDP_CANCEL:
21 | return carefullyMapValues(state, (method, methodId, remove) => {
22 | if (methodId === action.meta.methodId) {
23 | return remove(methodId);
24 | }
25 | return method;
26 | });
27 | case DDP_ENQUEUE: {
28 | if (action.meta.methodId) {
29 | const { methodId } = action.meta;
30 | return {
31 | ...state,
32 | [methodId]: {
33 | ...state[methodId],
34 | id: methodId,
35 | state: DDP_METHOD_STATE__QUEUED,
36 | },
37 | };
38 | }
39 | return state;
40 | }
41 | case DDP_METHOD:
42 | return {
43 | ...state,
44 | [id]: {
45 | ...state[id],
46 | id,
47 | state: DDP_METHOD_STATE__PENDING,
48 | name: action.payload.method,
49 | params: action.payload.params,
50 | ...extractMetadata(action.meta),
51 | },
52 | };
53 | case DDP_RESULT:
54 | return state[id] && state[id].state === DDP_METHOD_STATE__PENDING
55 | ? {
56 | ...state,
57 | [id]: {
58 | ...state[id],
59 | state: DDP_METHOD_STATE__RETURNED,
60 | result: action.payload.result,
61 | error: action.payload.error,
62 | },
63 | }
64 | : omit(state, id);
65 | case DDP_UPDATED:
66 | return carefullyMapValues(state, (method, methodId, remove) => {
67 | if (action.payload.methods.indexOf(methodId) < 0) {
68 | return method;
69 | }
70 | if (method.state === DDP_METHOD_STATE__RETURNED) {
71 | return remove(methodId);
72 | }
73 | return {
74 | ...method,
75 | state: DDP_METHOD_STATE__UPDATED,
76 | };
77 | });
78 | default:
79 | return state;
80 | }
81 | };
82 |
83 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/reducer.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | /* eslint no-invalid-this: "off" */
3 |
4 | import { createReducer } from './reducer';
5 | import {
6 | DDP_ENQUEUE,
7 | DDP_METHOD,
8 | DDP_UPDATED,
9 | DDP_RESULT,
10 | DDP_CANCEL,
11 |
12 | DDP_METHOD_STATE__QUEUED,
13 | DDP_METHOD_STATE__PENDING,
14 | DDP_METHOD_STATE__UPDATED,
15 | DDP_METHOD_STATE__RETURNED,
16 | } from '../../constants';
17 | import { DDPClient } from './testCommon';
18 |
19 | describe('Test module - methods - reducer', () => {
20 | let testContext;
21 |
22 | beforeEach(() => {
23 | testContext = {};
24 | });
25 |
26 | beforeEach(() => {
27 | testContext.reducer = createReducer(DDPClient);
28 | });
29 |
30 | test('should initialize state', () => {
31 | expect(testContext.reducer(undefined, {})).toEqual({});
32 | });
33 |
34 | test('should add new method', () => {
35 | expect(testContext.reducer({}, {
36 | type: DDP_METHOD,
37 | payload: {
38 | id: '1',
39 | method: 'methodA',
40 | params: [1, 2, 3],
41 | },
42 | meta: {
43 | methodId: '1',
44 | socketId: 'socket/1',
45 | },
46 | })).toEqual({
47 | 1: {
48 | id: '1',
49 | name: 'methodA',
50 | params: [1, 2, 3],
51 | state: DDP_METHOD_STATE__PENDING,
52 | socketId: 'socket/1',
53 | },
54 | });
55 | });
56 |
57 | test('should add method in "queued" state', () => {
58 | expect(testContext.reducer({}, {
59 | type: DDP_ENQUEUE,
60 | payload: {
61 | },
62 | meta: {
63 | type: DDP_METHOD,
64 | methodId: '1',
65 | },
66 | })).toEqual({
67 | 1: {
68 | id: '1',
69 | state: DDP_METHOD_STATE__QUEUED,
70 | },
71 | });
72 | });
73 |
74 | test(
75 | 'should switch state from "queued" to "pending" on DDP_METHOD',
76 | () => {
77 | expect(testContext.reducer({
78 | 1: {
79 | state: DDP_METHOD_STATE__QUEUED,
80 | },
81 | }, {
82 | type: DDP_METHOD,
83 | payload: {
84 | method: 'methodA',
85 | params: [],
86 | },
87 | meta: {
88 | methodId: '1',
89 | },
90 | })).toEqual({
91 | 1: {
92 | id: '1',
93 | state: DDP_METHOD_STATE__PENDING,
94 | name: 'methodA',
95 | params: [],
96 | },
97 | });
98 | },
99 | );
100 |
101 | test('should change method state to "returned"', () => {
102 | expect(testContext.reducer({
103 | 1: {
104 | id: '1',
105 | state: DDP_METHOD_STATE__PENDING,
106 | },
107 | }, {
108 | type: DDP_RESULT,
109 | payload: {
110 | id: '1',
111 | },
112 | meta: {
113 | methodId: '1',
114 | },
115 | })).toEqual({
116 | 1: {
117 | id: '1',
118 | state: DDP_METHOD_STATE__RETURNED,
119 | result: undefined,
120 | error: undefined,
121 | },
122 | });
123 | });
124 |
125 | test('should change method state to "updated"', () => {
126 | expect(testContext.reducer({
127 | 1: {
128 | id: '1',
129 | state: DDP_METHOD_STATE__PENDING,
130 | },
131 | }, {
132 | type: DDP_UPDATED,
133 | payload: {
134 | methods: ['1'],
135 | },
136 | meta: {},
137 | })).toEqual({
138 | 1: {
139 | id: '1',
140 | state: DDP_METHOD_STATE__UPDATED,
141 | },
142 | });
143 | });
144 |
145 | test('should remove method if it is already updated', () => {
146 | expect(testContext.reducer({
147 | 1: {
148 | id: '1',
149 | state: DDP_METHOD_STATE__UPDATED,
150 | },
151 | }, {
152 | type: DDP_RESULT,
153 | payload: {
154 | id: '1',
155 | },
156 | meta: {
157 | methodId: '1',
158 | },
159 | })).toEqual({});
160 | });
161 |
162 | test('should remove method if it already returned', () => {
163 | expect(testContext.reducer({
164 | 1: {
165 | id: '1',
166 | state: DDP_METHOD_STATE__RETURNED,
167 | },
168 | }, {
169 | type: DDP_UPDATED,
170 | payload: {
171 | methods: ['1', '2'],
172 | },
173 | })).toEqual({});
174 | });
175 |
176 | test('should remove method if it is canceled', () => {
177 | expect(testContext.reducer({
178 | 1: {
179 | id: '1',
180 | state: DDP_METHOD_STATE__PENDING,
181 | },
182 | }, {
183 | type: DDP_CANCEL,
184 | meta: {
185 | methodId: '1',
186 | },
187 | })).toEqual({});
188 | });
189 | });
190 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/selectors.js:
--------------------------------------------------------------------------------
1 | import map from 'lodash/map';
2 | import { createSelector } from 'reselect';
3 | import { DDP_METHOD_STATE__READY } from '../../constants';
4 |
5 | /**
6 | * Create a selector that returns a list representing
7 | * current state of the methods user is interested in.
8 | * @param {Object} options
9 | * @param {Function} options.selectMethodsIds
10 | */
11 | export const createMethodsSelector = ({
12 | selectMethodsIds,
13 | readyState = {
14 | state: DDP_METHOD_STATE__READY,
15 | },
16 | }) => createSelector(
17 | selectMethodsIds,
18 | state => state.ddp && state.ddp.methods,
19 | // NOTE: If method is not present in the store, it means that's already completed.
20 | (methodsIds, state) => map(methodsIds, id => (id && state[id]) || readyState),
21 | );
22 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/methods/testCommon.js:
--------------------------------------------------------------------------------
1 | /* eslint class-methods-use-this: "off" */
2 | import DDPEmitter from '../../DDPEmitter';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | nextUniqueId() {
6 | return '1';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries/reducer.js:
--------------------------------------------------------------------------------
1 | import has from 'lodash/has';
2 | import omit from 'lodash/omit';
3 | import {
4 | DDP_STATE__INITIAL,
5 | DDP_STATE__QUEUED,
6 | DDP_STATE__PENDING,
7 | DDP_STATE__READY,
8 | DDP_STATE__OBSOLETE,
9 | DDP_STATE__RESTORING,
10 | DDP_STATE__CANCELED,
11 |
12 | DDP_ENQUEUE,
13 | DDP_DISCONNECTED,
14 |
15 | DDP_QUERY_REQUEST,
16 | DDP_QUERY_RELEASE,
17 |
18 | DDP_QUERY_CREATE,
19 | DDP_QUERY_DELETE,
20 | DDP_QUERY_UPDATE,
21 | DDP_QUERY_REFETCH,
22 | } from '../../constants';
23 |
24 | const setProperty = propName => (value) => {
25 | if (typeof value === 'function') {
26 | return (state) => {
27 | const valueToSet = value(state[propName]);
28 | return (state[propName] === valueToSet
29 | ? state
30 | : {
31 | ...state,
32 | [propName]: valueToSet,
33 | }
34 | );
35 | };
36 | }
37 | return state => (state[propName] === value
38 | ? state
39 | : {
40 | ...state,
41 | [propName]: value,
42 | }
43 | );
44 | };
45 |
46 | const increaseBy = value => (currentValue = 0) => currentValue + value;
47 |
48 | const increaseProperty = propName => value => setProperty(propName)(increaseBy(value));
49 |
50 | const setState = setProperty('state');
51 | const deleteKey = (object, key) => {
52 | if (has(object, key)) {
53 | return omit(object, key);
54 | }
55 | return object;
56 | };
57 |
58 | const increaseUsersByOne = increaseProperty('users')(1);
59 | const decreaseUsersByOne = increaseProperty('users')(-1);
60 |
61 | const queryReducer = (state = {
62 | state: DDP_STATE__INITIAL,
63 | }, action) => {
64 | switch (action.type) {
65 | case DDP_QUERY_REQUEST:
66 | return increaseUsersByOne(state);
67 | case DDP_QUERY_RELEASE:
68 | return decreaseUsersByOne(state);
69 | case DDP_ENQUEUE: {
70 | switch (state.state) {
71 | case DDP_STATE__INITIAL:
72 | return setState(DDP_STATE__QUEUED)(state);
73 | default:
74 | return state;
75 | }
76 | }
77 | case DDP_DISCONNECTED: {
78 | switch (state.state) {
79 | case DDP_STATE__PENDING:
80 | return setState(DDP_STATE__CANCELED)(state);
81 | case DDP_STATE__READY:
82 | case DDP_STATE__RESTORING:
83 | return setState(DDP_STATE__OBSOLETE)(state);
84 | default:
85 | return state;
86 | }
87 | }
88 | case DDP_QUERY_CREATE:
89 | return {
90 | ...state,
91 | id: action.meta.queryId,
92 | name: action.payload.name,
93 | params: action.payload.params,
94 | properties: action.payload.properties,
95 | };
96 | case DDP_QUERY_REFETCH:
97 | return state.users > 0 ? state : setState(DDP_STATE__OBSOLETE)(state);
98 | case DDP_QUERY_UPDATE: {
99 | if (!action.payload) {
100 | switch (state.state) {
101 | case DDP_STATE__INITIAL:
102 | case DDP_STATE__QUEUED:
103 | return setState(DDP_STATE__PENDING)(state);
104 | case DDP_STATE__READY:
105 | return setState(DDP_STATE__RESTORING)(state);
106 | default:
107 | return state;
108 | }
109 | }
110 | const {
111 | error,
112 | result,
113 | entities,
114 | } = action.payload;
115 | if (error) {
116 | return {
117 | ...state,
118 | error,
119 | state: DDP_STATE__CANCELED,
120 | };
121 | }
122 | return {
123 | ...state,
124 | ...entities !== undefined && { entities },
125 | result,
126 | state: DDP_STATE__READY,
127 | };
128 | }
129 | default:
130 | return state;
131 | }
132 | };
133 |
134 | export const createReducer = () => (state = {}, action) => {
135 | switch (action.type) {
136 | case DDP_ENQUEUE:
137 | case DDP_QUERY_REQUEST:
138 | case DDP_QUERY_RELEASE:
139 | case DDP_QUERY_UPDATE:
140 | case DDP_QUERY_REFETCH:
141 | case DDP_DISCONNECTED: {
142 | const queryId = action.meta &&
143 | action.meta.queryId;
144 | if (queryId) {
145 | const queryState = state[queryId];
146 | return {
147 | ...state,
148 | [queryId]: queryReducer(queryState, action),
149 | };
150 | }
151 | return state;
152 | }
153 | case DDP_QUERY_DELETE:
154 | return deleteKey(state, action.meta.queryId);
155 | case DDP_QUERY_CREATE: {
156 | const queryId = action.meta &&
157 | action.meta.queryId;
158 | if (queryId) {
159 | return {
160 | ...state,
161 | [queryId]: queryReducer(state[queryId], action),
162 | };
163 | }
164 | return state;
165 | }
166 | default:
167 | return state;
168 | }
169 | };
170 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries/selectors.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import { createSelector } from 'reselect';
3 | import stableMap from '../../utils/stableMap';
4 | import EJSON from '../../ejson';
5 | import {
6 | DEFAULT_SOCKET_ID,
7 | DDP_STATE__READY,
8 | } from '../../constants';
9 |
10 | const constant = x => () => x;
11 |
12 | export const findQuery = (queries, name, params, properties) => find(
13 | queries,
14 | x => x.name === name &&
15 | EJSON.equals(x.params, params) &&
16 | EJSON.equals(x.properties, properties),
17 | );
18 |
19 | /**
20 | * Create a selector that returns a list (or object) representing
21 | * current state of the queries user is interested in.
22 | * @param {Object} options
23 | * @param {Function} options.selectQueriesRequests
24 | * @param {Function} options.selectConnectionId
25 | * @param {Function} options.emptyState - this is used to distinguish between missing subscription and no request at all
26 | */
27 | export const createQueriesSelector = ({
28 | selectDeclaredQueries,
29 | selectConnectionId = constant(DEFAULT_SOCKET_ID),
30 | emptyState = {
31 | state: DDP_STATE__READY,
32 | },
33 | }) => createSelector(
34 | selectDeclaredQueries,
35 | selectConnectionId,
36 | state => state.ddp && state.ddp.queries,
37 | (declaredQueries, socketId, queries) => (socketId
38 | ? stableMap(declaredQueries, y => (y
39 | ? findQuery(
40 | queries,
41 | y.name,
42 | y.params,
43 | {
44 | socketId,
45 | ...y.properties,
46 | },
47 | )
48 | : emptyState
49 | ))
50 | : stableMap(declaredQueries, constant(null))
51 | ),
52 | );
53 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 | import { callMethod } from '../../actions';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | getQueryCleanupTimeout() {
6 | return this.constructor.getQueryCleanupTimeout();
7 | }
8 |
9 | nextUniqueId() {
10 | return this.constructor.defaultUniqueId;
11 | }
12 |
13 | extractEntities(result) {
14 | return this.constructor.extractEntities(result);
15 | }
16 |
17 | fetch(...args) {
18 | return this.constructor.fetch(...args);
19 | }
20 |
21 | static getQueryCleanupTimeout() {
22 | return 1000;
23 | }
24 |
25 | static extractEntities(result) {
26 | return result.entities;
27 | }
28 |
29 | static fetch(...args) {
30 | return callMethod(...args);
31 | }
32 | }
33 |
34 | DDPClient.defaultUniqueId = '1';
35 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/middleware.js:
--------------------------------------------------------------------------------
1 | import {
2 | DDP_RESOURCE_UPDATE,
3 | DDP_RESOURCE_DELETE,
4 | DDP_RESOURCE_FETCH,
5 | DDP_RESOURCE_REFETCH,
6 | } from '../../constants';
7 | import {
8 | callMethod,
9 | updateResource,
10 | } from '../../actions';
11 |
12 | /**
13 | * Create middleware for the given ddpClient.
14 | * @param {DDPClient} ddpClient
15 | */
16 | export const createMiddleware = ddpClient => store => next => (action) => {
17 | if (!action ||
18 | typeof action !== 'object' ||
19 | !action.payload ||
20 | !action.meta) {
21 | return next(action);
22 | }
23 | switch (action.type) {
24 | case DDP_RESOURCE_FETCH:
25 | case DDP_RESOURCE_REFETCH:
26 | case DDP_RESOURCE_DELETE:
27 | case DDP_RESOURCE_UPDATE: {
28 | const type = action.payload.properties &&
29 | action.payload.properties.type;
30 | if (type !== 'query') {
31 | return next(action);
32 | }
33 | break;
34 | }
35 | default:
36 | return next(action);
37 | }
38 | const {
39 | name,
40 | params,
41 | properties,
42 | } = action.payload;
43 | const {
44 | resourceId,
45 | } = action.meta;
46 |
47 | switch (action.type) {
48 | case DDP_RESOURCE_FETCH:
49 | case DDP_RESOURCE_REFETCH: {
50 | const {
51 | socketId,
52 | } = properties;
53 | const nextResult = next(action);
54 |
55 | store.dispatch(callMethod(name, params, {
56 | resourceId,
57 | socketId,
58 | })).then((result) => {
59 | store.dispatch(updateResource(resourceId, { result }));
60 | }).catch((error) => {
61 | store.dispatch(updateResource(resourceId, { error }));
62 | });
63 |
64 | return nextResult;
65 | }
66 | case DDP_RESOURCE_DELETE: {
67 | const state = store.getState();
68 | const resource = resourceId && state.ddp.resources[resourceId];
69 | if (!resource) {
70 | return next(action);
71 | }
72 | return next({
73 | ...action,
74 | payload: {
75 | entities: resource.entities,
76 | ...action.payload,
77 | },
78 | });
79 | }
80 | case DDP_RESOURCE_UPDATE: {
81 | const state = store.getState();
82 | const resource = resourceId && state.ddp.resources[resourceId];
83 | if (!resource) {
84 | return next(action);
85 | }
86 | const newAction = {
87 | ...action,
88 | payload: {
89 | ...action.payload,
90 | oldEntities: resource.entities,
91 | },
92 | };
93 | if (action.payload &&
94 | action.payload.result &&
95 | typeof action.payload.result === 'object'
96 | ) {
97 | newAction.payload.entities = ddpClient.extractEntities(
98 | action.payload.result,
99 | {
100 | name,
101 | params,
102 | properties,
103 | },
104 | );
105 | }
106 | return next(newAction);
107 | }
108 | default:
109 | return next(action);
110 | }
111 | };
112 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/reducer.js:
--------------------------------------------------------------------------------
1 | export const createReducer = () => (state = {}) => state;
2 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/reducer.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | /* eslint no-invalid-this: "off" */
3 |
4 | import { createReducer } from './reducer';
5 | import { DDPClient } from './testCommon';
6 |
7 | describe('Test module - queries2 - reducer', () => {
8 | let testContext;
9 |
10 | beforeEach(() => {
11 | testContext = {};
12 | });
13 |
14 | beforeEach(() => {
15 | testContext.reducer = createReducer(DDPClient);
16 | });
17 |
18 | test('should initialize state', () => {
19 | expect(testContext.reducer(undefined, {})).toEqual({});
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/selectors.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import { createSelector } from 'reselect';
3 | import stableMap from '../../utils/stableMap';
4 | import EJSON from '../../ejson';
5 | import {
6 | DEFAULT_SOCKET_ID,
7 | DDP_STATE__READY,
8 | } from '../../constants';
9 |
10 | const constant = x => () => x;
11 |
12 | export const findQuery = (queries, name, params, properties) => find(
13 | queries,
14 | x => x.name === name &&
15 | EJSON.equals(x.params, params) &&
16 | EJSON.equals(x.properties, properties),
17 | );
18 |
19 | /**
20 | * Create a selector that returns a list (or object) representing
21 | * current state of the queries user is interested in.
22 | * @param {Object} options
23 | * @param {Function} options.selectQueriesRequests
24 | * @param {Function} options.selectConnectionId
25 | * @param {Function} options.emptyState - this is used to distinguish between missing subscription and no request at all
26 | */
27 | export const createQueriesSelector = ({
28 | selectDeclaredQueries,
29 | selectConnectionId = constant(DEFAULT_SOCKET_ID),
30 | emptyState = {
31 | state: DDP_STATE__READY,
32 | },
33 | }) => createSelector(
34 | selectDeclaredQueries,
35 | selectConnectionId,
36 | state => state.ddp && state.ddp.queries,
37 | (declaredQueries, socketId, queries) => (socketId
38 | ? stableMap(declaredQueries, y => (y
39 | ? findQuery(
40 | queries,
41 | y.name,
42 | y.params,
43 | {
44 | socketId,
45 | ...y.properties,
46 | },
47 | )
48 | : emptyState
49 | ))
50 | : stableMap(declaredQueries, constant(null))
51 | ),
52 | );
53 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queries2/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 | import { callMethod } from '../../actions';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | getCleanupTimeout() {
6 | return this.constructor.getQueryCleanupTimeout();
7 | }
8 |
9 | nextUniqueId() {
10 | return this.constructor.defaultUniqueId;
11 | }
12 |
13 | extractEntities(result) {
14 | return this.constructor.extractEntities(result);
15 | }
16 |
17 | fetch(...args) {
18 | return this.constructor.fetch(...args);
19 | }
20 |
21 | static getQueryCleanupTimeout() {
22 | return 1000;
23 | }
24 |
25 | static extractEntities(result) {
26 | return result.entities;
27 | }
28 |
29 | static fetch(...args) {
30 | return callMethod(...args);
31 | }
32 | }
33 |
34 | DDPClient.defaultUniqueId = '1';
35 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/constants.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apendua/ddp-redux/412b4e38de09d398ad7bcc964f920f6fcf907396/ddp-redux/src/modules/queues/constants.js
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/middleware.js:
--------------------------------------------------------------------------------
1 | import max from 'lodash/max';
2 | import values from 'lodash/values';
3 | import { DDP_ENQUEUE } from '../../constants';
4 |
5 | /**
6 | * Return the maximal priority of the current pending actions.
7 | * @param {object} state
8 | * @param {string} queueId
9 | * @returns {number}
10 | */
11 | const getMaxPendingValue = (state, queueId) => {
12 | const pendingValues = values(state.ddp.queues[queueId] &&
13 | state.ddp.queues[queueId].pending);
14 | if (pendingValues.length === 0) {
15 | return -Infinity;
16 | }
17 | return max(pendingValues);
18 | };
19 |
20 | /**
21 | * Create middleware for the given ddpClient.
22 | * @param {DDPClient} ddpClient
23 | */
24 | export const createMiddleware = () => store => next => (action) => {
25 | if (!action || typeof action !== 'object' || action.type === DDP_ENQUEUE) {
26 | return next(action);
27 | }
28 | const queueId = action.meta &&
29 | action.meta.queue &&
30 | action.meta.queue.id;
31 | if (!queueId) {
32 | return next(action);
33 | }
34 | const {
35 | resolve,
36 | priority = 0,
37 | } = action.meta.queue;
38 | if (resolve) {
39 | // NOTE: We are propagating action first, because
40 | // we want to get an up-to-date threshold.
41 | const result = next(action);
42 | const state = store.getState();
43 | const elements = state.ddp.queues[queueId] &&
44 | state.ddp.queues[queueId].elements;
45 | if (elements) {
46 | let t = getMaxPendingValue(state, queueId);
47 | let i = 0;
48 | while (i < elements.length && t <= (elements[i].meta.queue.priority || 0)) {
49 | store.dispatch(elements[i]);
50 | // Note that threshold might have changed after dispatching another action.
51 | t = getMaxPendingValue(store.getState(), queueId);
52 | i += 1;
53 | }
54 | }
55 | return result;
56 | }
57 | if (priority >= getMaxPendingValue(store.getState(), queueId)) {
58 | return next(action);
59 | }
60 | return store.dispatch({
61 | ...action,
62 | type: DDP_ENQUEUE,
63 | meta: {
64 | ...action.meta,
65 | type: action.type,
66 | },
67 | });
68 | };
69 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/middleware.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import configureStore from 'redux-mock-store';
4 | import { createMiddleware } from './middleware';
5 | import { DDP_ENQUEUE } from '../../constants';
6 |
7 | const createInitialState = (queueId, queueState) => ({
8 | ddp: {
9 | queues: {
10 | [queueId]: queueState,
11 | },
12 | },
13 | });
14 |
15 | describe('Test module - queues - middleware', () => {
16 | let testContext;
17 |
18 | beforeEach(() => {
19 | testContext = {};
20 | });
21 |
22 | beforeEach(() => {
23 | testContext.send = jest.fn();
24 | testContext.onError = jest.fn();
25 | testContext.middleware = createMiddleware();
26 | testContext.mockStore = configureStore([
27 | testContext.middleware,
28 | ]);
29 | });
30 |
31 | test('should pass an action without queue meta field', () => {
32 | const store = testContext.mockStore();
33 | const action = {
34 | type: 'no_meta_queue',
35 | payload: {},
36 | meta: {},
37 | };
38 | store.dispatch(action);
39 | expect(store.getActions()).toEqual(expect.arrayContaining([
40 | action,
41 | ]));
42 | });
43 |
44 | test('should enqueue action if priority is too low', () => {
45 | const store = testContext.mockStore(createInitialState('1', {
46 | pending: {
47 | 1: 20,
48 | 2: 30,
49 | },
50 | elements: [],
51 | }));
52 | const action = {
53 | type: 'action',
54 | payload: {
55 | id: '2',
56 | },
57 | meta: {
58 | queue: {
59 | id: '1',
60 | priority: 25,
61 | },
62 | },
63 | };
64 | store.dispatch(action);
65 | expect(store.getActions()).toEqual([{
66 | type: DDP_ENQUEUE,
67 | payload: action.payload,
68 | meta: {
69 | type: action.type,
70 | ...action.meta,
71 | },
72 | }]);
73 | });
74 |
75 | test('should empty queue up to the computed threshold', () => {
76 | const store = testContext.mockStore(createInitialState('1', {
77 | pending: {
78 | 1: 10,
79 | 2: 0,
80 | },
81 | elements: [{
82 | type: 'action',
83 | payload: {
84 | id: '3',
85 | },
86 | meta: {
87 | queue: {
88 | id: '1',
89 | priority: 10,
90 | },
91 | },
92 | }, {
93 | type: 'action',
94 | payload: {
95 | id: '4',
96 | },
97 | meta: {
98 | queue: {
99 | id: '1',
100 | priority: 10,
101 | },
102 | },
103 | }, {
104 | type: 'action',
105 | payload: {
106 | id: '5',
107 | },
108 | meta: {
109 | queue: {
110 | id: '1',
111 | priority: 0,
112 | },
113 | },
114 | }],
115 | }));
116 | const action = {
117 | type: 'action',
118 | payload: 4,
119 | meta: {
120 | queue: {
121 | id: '1',
122 | resolve: true,
123 | },
124 | },
125 | };
126 | store.dispatch(action);
127 | expect(store.getActions()).toEqual([
128 | action,
129 | {
130 | type: 'action',
131 | payload: {
132 | id: '3',
133 | },
134 | meta: {
135 | queue: {
136 | id: '1',
137 | priority: 10,
138 | },
139 | },
140 | },
141 | {
142 | type: 'action',
143 | payload: {
144 | id: '4',
145 | },
146 | meta: {
147 | queue: {
148 | id: '1',
149 | priority: 10,
150 | },
151 | },
152 | },
153 | ]);
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/reducer.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import findIndex from 'lodash/findIndex';
3 | import findLastIndex from 'lodash/findLastIndex';
4 | import {
5 | DDP_ENQUEUE,
6 | DDP_QUEUE_RESET,
7 | } from '../../constants';
8 |
9 | export const queueReducer = (state = {
10 | elements: [],
11 | pending: {},
12 | }, action) => {
13 | switch (action.type) {
14 | case DDP_QUEUE_RESET:
15 | return {
16 | ...state,
17 | pending: (action.payload &&
18 | action.payload.initialPending) || {},
19 | };
20 | case DDP_ENQUEUE:
21 | return {
22 | ...state,
23 | elements: ((elements) => {
24 | // 1. elements with higher priority go first
25 | // 2. if priority is not specified, we assume 0
26 | const priority = action.meta.queue.priority || 0;
27 | const index = findLastIndex(elements, el => priority <= (el.meta.queue.priority || 0));
28 | const {
29 | type,
30 | ...meta
31 | } = action.meta;
32 | return [
33 | ...elements.slice(0, index + 1),
34 | {
35 | type,
36 | meta,
37 | payload: action.payload,
38 | },
39 | ...elements.slice(index + 1),
40 | ];
41 | })(state.elements),
42 | };
43 | default: {
44 | const {
45 | resolve,
46 | elementId,
47 | } = action.meta.queue;
48 | if (!elementId) {
49 | return state;
50 | }
51 | if (resolve) {
52 | return {
53 | ...state,
54 | pending: omit(state.pending, elementId),
55 | };
56 | }
57 | const index = findIndex(state.elements, el => el.meta.queue.elementId === elementId);
58 | if (index < 0) {
59 | return state;
60 | }
61 | const { pendingValue } = state.elements[index].meta.queue;
62 | return {
63 | ...state,
64 | ...pendingValue !== undefined && {
65 | pending: {
66 | ...state.pending,
67 | [elementId]: pendingValue,
68 | },
69 | },
70 | elements: [
71 | ...state.elements.slice(0, index),
72 | ...state.elements.slice(index + 1),
73 | ],
74 | };
75 | }
76 | }
77 | };
78 |
79 | export const createReducer = () => (state = {}, action) => {
80 | const queueId = action.meta &&
81 | action.meta.queue &&
82 | action.meta.queue.id;
83 | if (!queueId) {
84 | return state;
85 | }
86 | const queue = state[queueId];
87 | if (!queue && action.type !== DDP_QUEUE_RESET) {
88 | return state;
89 | }
90 | return {
91 | ...state,
92 | [queueId]: queueReducer(queue, action),
93 | };
94 | };
95 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/queues/selectors.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apendua/ddp-redux/412b4e38de09d398ad7bcc964f920f6fcf907396/ddp-redux/src/modules/queues/selectors.js
--------------------------------------------------------------------------------
/ddp-redux/src/modules/resources/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/resources/reducer.js:
--------------------------------------------------------------------------------
1 | import has from 'lodash/has';
2 | import omit from 'lodash/omit';
3 | import {
4 | DDP_STATE__INITIAL,
5 | DDP_STATE__PENDING,
6 | DDP_STATE__READY,
7 | DDP_STATE__OBSOLETE,
8 | DDP_STATE__RESTORING,
9 | DDP_STATE__CANCELED,
10 |
11 | DDP_DISCONNECTED,
12 |
13 | DDP_RESOURCE_REQUEST,
14 | DDP_RESOURCE_RELEASE,
15 | DDP_RESOURCE_REFETCH,
16 | DDP_RESOURCE_FETCH,
17 |
18 | DDP_RESOURCE_CREATE,
19 | DDP_RESOURCE_DELETE,
20 | DDP_RESOURCE_UPDATE,
21 | DDP_RESOURCE_DEPRECATE,
22 | } from '../../constants';
23 |
24 | const setProperty = propName => (value) => {
25 | if (typeof value === 'function') {
26 | return (state) => {
27 | const valueToSet = value(state[propName]);
28 | return (
29 | state[propName] === valueToSet
30 | ? state
31 | : {
32 | ...state,
33 | [propName]: valueToSet,
34 | }
35 | );
36 | };
37 | }
38 | return state => (
39 | state[propName] === value
40 | ? state
41 | : {
42 | ...state,
43 | [propName]: value,
44 | }
45 | );
46 | };
47 |
48 | const increaseBy = value => (currentValue = 0) => currentValue + value;
49 | const increaseProperty = propName => value => setProperty(propName)(increaseBy(value));
50 |
51 | const setState = setProperty('state');
52 | const deleteKey = (object, key) => {
53 | if (has(object, key)) {
54 | return omit(object, key);
55 | }
56 | return object;
57 | };
58 |
59 | const increaseUsersByOne = increaseProperty('users')(1);
60 | const decreaseUsersByOne = increaseProperty('users')(-1);
61 |
62 | const resourceReducer = (state = {
63 | state: DDP_STATE__INITIAL,
64 | }, action) => {
65 | switch (action.type) {
66 | case DDP_RESOURCE_REQUEST:
67 | return increaseUsersByOne(state);
68 | case DDP_RESOURCE_RELEASE:
69 | return decreaseUsersByOne(state);
70 | case DDP_DISCONNECTED: {
71 | switch (state.state) {
72 | case DDP_STATE__PENDING:
73 | return setState(DDP_STATE__CANCELED)(state);
74 | case DDP_STATE__READY:
75 | case DDP_STATE__RESTORING:
76 | return setState(DDP_STATE__OBSOLETE)(state);
77 | default:
78 | return state;
79 | }
80 | }
81 | case DDP_RESOURCE_CREATE:
82 | return {
83 | ...state,
84 | id: action.meta.resourceId,
85 | name: action.payload.name,
86 | params: action.payload.params,
87 | properties: action.payload.properties,
88 | };
89 | case DDP_RESOURCE_DEPRECATE:
90 | return state.users > 0 ? state : setState(DDP_STATE__OBSOLETE)(state);
91 | case DDP_RESOURCE_REFETCH:
92 | case DDP_RESOURCE_FETCH: {
93 | switch (state.state) {
94 | case DDP_STATE__INITIAL:
95 | return setState(DDP_STATE__PENDING)(state);
96 | case DDP_STATE__READY:
97 | return setState(DDP_STATE__RESTORING)(state);
98 | default:
99 | return state;
100 | }
101 | }
102 | case DDP_RESOURCE_UPDATE: {
103 | if (!action.payload) {
104 | return state;
105 | }
106 | const {
107 | error,
108 | ...other
109 | } = action.payload;
110 | if (error) {
111 | return {
112 | ...state,
113 | error,
114 | state: DDP_STATE__CANCELED,
115 | };
116 | }
117 | return {
118 | ...state,
119 | ...other,
120 | state: DDP_STATE__READY,
121 | };
122 | }
123 | default:
124 | return state;
125 | }
126 | };
127 |
128 | export const createReducer = () => (state = {}, action) => {
129 | switch (action.type) {
130 | case DDP_RESOURCE_REQUEST:
131 | case DDP_RESOURCE_RELEASE:
132 | case DDP_RESOURCE_UPDATE:
133 | case DDP_RESOURCE_REFETCH:
134 | case DDP_RESOURCE_DEPRECATE:
135 | case DDP_RESOURCE_FETCH:
136 | case DDP_DISCONNECTED: {
137 | const resourceId = action.meta &&
138 | action.meta.resourceId;
139 | if (resourceId) {
140 | const resourceState = state[resourceId];
141 | return {
142 | ...state,
143 | [resourceId]: resourceReducer(resourceState, action),
144 | };
145 | }
146 | return state;
147 | }
148 | case DDP_RESOURCE_DELETE:
149 | return deleteKey(state, action.meta.resourceId);
150 | case DDP_RESOURCE_CREATE: {
151 | const resourceId = action.meta &&
152 | action.meta.resourceId;
153 | if (resourceId) {
154 | return {
155 | ...state,
156 | [resourceId]: resourceReducer(state[resourceId], action),
157 | };
158 | }
159 | return state;
160 | }
161 | default:
162 | return state;
163 | }
164 | };
165 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/resources/selectors.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import { createSelector } from 'reselect';
3 | import stableMap from '../../utils/stableMap';
4 | import EJSON from '../../ejson';
5 | import {
6 | DEFAULT_SOCKET_ID,
7 | DDP_STATE__READY,
8 | } from '../../constants';
9 |
10 | const constant = x => () => x;
11 |
12 | export const findResource = (resources, name, params, properties) => find(
13 | resources,
14 | x => x.name === name &&
15 | EJSON.equals(x.properties, properties) &&
16 | EJSON.equals(x.params, params),
17 | );
18 |
19 | /**
20 | * Create a selector that returns a list (or object) representing
21 | * current state of the resources user is interested in.
22 | * @param {Object} options
23 | * @param {Function} options.selectDeclaredResources
24 | * @param {Function} options.selectConnectionId
25 | * @param {Function} options.emptyState
26 | */
27 | export const createResourcesSelector = ({
28 | selectDeclaredResources,
29 | selectConnectionId = constant(DEFAULT_SOCKET_ID),
30 | emptyState = {
31 | state: DDP_STATE__READY,
32 | },
33 | }) => createSelector(
34 | selectDeclaredResources,
35 | selectConnectionId,
36 | state => state.ddp && state.ddp.resources,
37 | (declaredResources, socketId, resources) => (socketId
38 | ? stableMap(declaredResources, y => (y
39 | ? findResource(
40 | resources,
41 | y.name,
42 | y.params,
43 | {
44 | socketId,
45 | ...y.properties,
46 | },
47 | )
48 | : emptyState
49 | ))
50 | : stableMap(declaredResources, constant(null))
51 | ),
52 | );
53 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/subscriptions/index.js:
--------------------------------------------------------------------------------
1 | import { createReducer } from './reducer';
2 | import { createMiddleware } from './middleware';
3 | import { createSelectors } from './selectors';
4 |
5 | export {
6 | createReducer,
7 | createMiddleware,
8 | createSelectors,
9 | };
10 |
11 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/subscriptions/reducer.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import carefullyMapValues from '../../utils/carefullyMapValues';
3 | import {
4 | DEFAULT_SOCKET_ID,
5 |
6 | DDP_SUBSCRIPTION_STATE__INITIAL,
7 | DDP_SUBSCRIPTION_STATE__QUEUED,
8 | DDP_SUBSCRIPTION_STATE__PENDING,
9 | DDP_SUBSCRIPTION_STATE__READY,
10 | DDP_SUBSCRIPTION_STATE__RESTORING,
11 |
12 | DDP_ENQUEUE,
13 | DDP_CONNECT,
14 | DDP_SUB,
15 | DDP_UNSUB,
16 | DDP_READY,
17 | DDP_NOSUB,
18 | DDP_SUBSCRIBE,
19 | DDP_UNSUBSCRIBE,
20 | } from '../../constants';
21 |
22 | export const createReducer = () => (state = {}, action) => {
23 | switch (action.type) {
24 | case DDP_SUBSCRIBE:
25 | return (() => {
26 | const sub = state[action.meta.subId];
27 | return {
28 | ...state,
29 | [action.meta.subId]: {
30 | id: (sub && sub.id) || action.meta.subId,
31 | state: (sub && sub.state) || DDP_SUBSCRIPTION_STATE__INITIAL,
32 | name: (sub && sub.name) || action.payload.name,
33 | params: (sub && sub.params) || action.payload.params,
34 | users: ((sub && sub.users) || 0) + 1,
35 | socketId: (sub && sub.socketId) || (action.meta && action.meta.socketId) || DEFAULT_SOCKET_ID,
36 | },
37 | };
38 | })();
39 | case DDP_UNSUBSCRIBE:
40 | return state[action.meta.subId]
41 | ? {
42 | ...state,
43 | [action.meta.subId]: {
44 | ...state[action.meta.subId],
45 | users: (state[action.meta.subId].users || 0) - 1,
46 | },
47 | }
48 | : state;
49 | case DDP_ENQUEUE: {
50 | const { subId } = action.meta;
51 | if (subId) {
52 | const sub = state[subId];
53 | if (sub) {
54 | switch (sub.state) {
55 | case DDP_SUBSCRIPTION_STATE__INITIAL:
56 | return {
57 | ...state,
58 | [subId]: {
59 | ...sub,
60 | state: DDP_SUBSCRIPTION_STATE__QUEUED,
61 | },
62 | };
63 | default:
64 | return state;
65 | }
66 | }
67 | }
68 | return state;
69 | }
70 | case DDP_SUB: {
71 | if (action.meta.subId) {
72 | const { subId } = action.meta;
73 | const query = state[subId];
74 | if (query) {
75 | switch (query.state) {
76 | case DDP_SUBSCRIPTION_STATE__INITIAL:
77 | case DDP_SUBSCRIPTION_STATE__QUEUED:
78 | return {
79 | ...state,
80 | [subId]: {
81 | ...query,
82 | state: DDP_SUBSCRIPTION_STATE__PENDING,
83 | },
84 | };
85 | case DDP_SUBSCRIPTION_STATE__READY:
86 | return {
87 | ...state,
88 | [subId]: {
89 | ...query,
90 | state: DDP_SUBSCRIPTION_STATE__RESTORING,
91 | },
92 | };
93 | default:
94 | return state;
95 | }
96 | }
97 | }
98 | return state;
99 | }
100 | case DDP_UNSUB:
101 | return omit(state, [action.meta.subId]);
102 | case DDP_NOSUB:
103 | // NOTE: If the subscription was deleted in the meantime, this will
104 | // have completely no effect.
105 | return carefullyMapValues(state, (sub, id) => {
106 | if (action.meta.subId === id) {
107 | return {
108 | ...sub,
109 | state: DDP_SUBSCRIPTION_STATE__READY,
110 | error: action.payload.error,
111 | };
112 | }
113 | return sub;
114 | });
115 | case DDP_READY:
116 | // NOTE: If the subscription was deleted in the meantime, this will
117 | // have completely no effect.
118 | return carefullyMapValues(state, (sub, id) => {
119 | if (action.payload.subs.indexOf(id) >= 0) {
120 | return {
121 | ...sub,
122 | state: DDP_SUBSCRIPTION_STATE__READY,
123 | };
124 | }
125 | return sub;
126 | });
127 | case DDP_CONNECT:
128 | return (() => {
129 | const socketId = action.meta && action.meta.socketId;
130 | return carefullyMapValues(state, (sub) => {
131 | if (sub.meta && sub.meta.socketId === socketId && sub.state === DDP_SUBSCRIPTION_STATE__READY) {
132 | return {
133 | ...sub,
134 | state: DDP_SUBSCRIPTION_STATE__RESTORING,
135 | };
136 | }
137 | return sub;
138 | });
139 | })();
140 | default:
141 | return state;
142 | }
143 | };
144 |
145 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/subscriptions/selectors.js:
--------------------------------------------------------------------------------
1 | import find from 'lodash/find';
2 | import { createSelector } from 'reselect';
3 | import stableMap from '../../utils/stableMap';
4 | import EJSON from '../../ejson';
5 | import {
6 | DEFAULT_SOCKET_ID,
7 | DDP_SUBSCRIPTION_STATE__READY,
8 | } from '../../constants';
9 |
10 | const constant = x => () => x;
11 |
12 | /**
13 | * Create a selector that returns a list (or object) representing
14 | * current state of the subscriptions user is interested in.
15 | * @param {Object} options
16 | * @param {Function} options.selectDeclaredSubscriptions
17 | * @param {Function} options.selectConnectionId
18 | * @param {Function} options.emptyState - this is used to distinguish between missing subscription and no request at all
19 | */
20 | export const createSubscriptionsSelector = ({
21 | selectDeclaredSubscriptions,
22 | selectConnectionId = constant(DEFAULT_SOCKET_ID),
23 | emptyState = {
24 | state: DDP_SUBSCRIPTION_STATE__READY,
25 | },
26 | }) => createSelector(
27 | selectDeclaredSubscriptions,
28 | selectConnectionId,
29 | state => state.ddp && state.ddp.subscriptions,
30 | (subscriptions, connectionId, state) => (connectionId
31 | ? stableMap(subscriptions, y => (y
32 | ? find(
33 | state,
34 | x => x.socketId === connectionId &&
35 | x.name === y.name &&
36 | EJSON.equals(x.params, y.params),
37 | )
38 | : emptyState
39 | ))
40 | : stableMap(subscriptions, constant(null))
41 | ),
42 | );
43 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/subscriptions/testCommon.js:
--------------------------------------------------------------------------------
1 | import DDPEmitter from '../../DDPEmitter';
2 |
3 | export class DDPClient {
4 | constructor() {
5 | this.socket = new DDPEmitter();
6 | }
7 |
8 | getSubscriptionCleanupTimeout() {
9 | return this.constructor.getSubscriptionCleanupTimeout();
10 | }
11 |
12 | nextUniqueId() {
13 | return this.constructor.defaultUniqueId;
14 | }
15 |
16 | static getSubscriptionCleanupTimeout() {
17 | return 1000;
18 | }
19 | }
20 |
21 | DDPClient.defaultUniqueId = '1';
22 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/thunk.js:
--------------------------------------------------------------------------------
1 | export const createMiddleware = () =>
2 | store => next => action => (typeof action === 'function'
3 | ? action(store.dispatch, store.getState)
4 | : next(action)
5 | );
6 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/wrapWithPromise/index.js:
--------------------------------------------------------------------------------
1 | import { createMiddleware } from './middleware';
2 |
3 | export { createMiddleware };
4 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/wrapWithPromise/middleware.js:
--------------------------------------------------------------------------------
1 | import forEach from 'lodash/forEach';
2 | import DDPError from '../../DDPError';
3 | import {
4 | DDP_CANCEL,
5 | DDP_METHOD,
6 | DDP_RESULT,
7 | DDP_UPDATED,
8 | DDP_ENQUEUE,
9 | DDP_READY,
10 | DDP_SUB,
11 | DDP_NOSUB,
12 |
13 | DDP_METHOD_STATE__UPDATED,
14 | DDP_METHOD_STATE__RETURNED,
15 | } from '../../constants';
16 |
17 | /**
18 | * Create middleware for the given ddpClient.
19 | * @param {DDPClient} ddpClient
20 | */
21 | export const createMiddleware = ddpClient => (store) => {
22 | const promises = {};
23 | /**
24 | * Create a promise bound to message id.
25 | * @param {String} id
26 | * @returns {Promise}
27 | */
28 | const createPromise = (id, meta) => {
29 | if (promises[id]) {
30 | return promises[id].promise;
31 | }
32 | promises[id] = {};
33 | promises[id].meta = meta;
34 | promises[id].promise = new Promise((resolve, reject) => {
35 | promises[id].fulfill = (err, res) => {
36 | delete promises[id];
37 | if (err) {
38 | reject(err);
39 | } else {
40 | resolve(res);
41 | }
42 | };
43 | });
44 | if (ddpClient.onPromise) {
45 | ddpClient.onPromise(promises[id].promise, promises[id].meta);
46 | }
47 | return promises[id].promise;
48 | };
49 | /**
50 | * Fulfill an existing promise, identified by message id.
51 | * @param {String} id
52 | * @param {Object} options
53 | * @param {Error} [options.error]
54 | * @param {*} [options.result]
55 | */
56 | const fulfill = (id, { error, result }) => {
57 | const promise = promises[id];
58 | if (promise) {
59 | promise.fulfill(
60 | ddpClient.cleanError(error),
61 | result,
62 | );
63 | }
64 | };
65 | return next => (action) => {
66 | if (!action || typeof action !== 'object') {
67 | return next(action);
68 | }
69 | switch (action.type) {
70 | case DDP_CANCEL:
71 | return (() => {
72 | // cancel can result in either resolving or rejecting the promise, depending on the "error" flag
73 | fulfill(action.meta.methodId, action.error
74 | ? {
75 | error: action.payload ||
76 | new DDPError(DDPError.ERROR_CANCELED, 'Method was canceled by user', action),
77 | }
78 | : { result: action.payload });
79 | return next(action);
80 | })();
81 | case DDP_ENQUEUE:
82 | case DDP_METHOD:
83 | case DDP_SUB:
84 | return (() => {
85 | let promise = null;
86 | if (action.type === DDP_ENQUEUE) {
87 | switch (action.meta.type) {
88 | case DDP_METHOD:
89 | case DDP_SUB:
90 | promise = createPromise(action.payload.id, action.meta);
91 | break;
92 | default:
93 | return next(action);
94 | }
95 | } else {
96 | promise = createPromise(action.payload.id, action.meta);
97 | }
98 | next(action);
99 | promise.id = action.payload.id;
100 | return promise;
101 | })();
102 | case DDP_RESULT:
103 | return (() => {
104 | const methodId = action.payload.id;
105 | const state = store.getState();
106 | const method = state.ddp.methods[methodId];
107 | if (method && method.state === DDP_METHOD_STATE__UPDATED) {
108 | fulfill(methodId, {
109 | error: action.payload.error, result: action.payload.result,
110 | });
111 | }
112 | return next(action);
113 | })();
114 | case DDP_UPDATED:
115 | return (() => {
116 | const state = store.getState();
117 | forEach(action.payload.methods, (methodId) => {
118 | const method = state.ddp.methods[methodId];
119 | if (method && method.state === DDP_METHOD_STATE__RETURNED) {
120 | fulfill(methodId, {
121 | error: method.error, result: method.result,
122 | });
123 | }
124 | });
125 | return next(action);
126 | })();
127 | case DDP_READY:
128 | forEach(action.payload.subs, (subId) => {
129 | fulfill(subId, {});
130 | });
131 | return next(action);
132 | case DDP_NOSUB:
133 | fulfill(action.payload.id, {
134 | error: action.payload.error,
135 | });
136 | return next(action);
137 | default:
138 | return next(action);
139 | }
140 | };
141 | };
142 |
--------------------------------------------------------------------------------
/ddp-redux/src/modules/wrapWithPromise/testCommon.js:
--------------------------------------------------------------------------------
1 | /* eslint class-methods-use-this: "off" */
2 | import DDPEmitter from '../../DDPEmitter';
3 |
4 | export class DDPClient extends DDPEmitter {
5 | nextUniqueId() {
6 | return '1';
7 | }
8 |
9 | cleanError(error) {
10 | if (!error) {
11 | return null;
12 | }
13 | if (typeof error === 'string') {
14 | return new Error(error);
15 | }
16 | return new Error(error.error);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ddp-redux/src/shim.js:
--------------------------------------------------------------------------------
1 | import flags from 'regexp.prototype.flags';
2 |
3 | if (!RegExp.prototype.flags) {
4 | flags.shim();
5 | }
6 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/Storage.js:
--------------------------------------------------------------------------------
1 | /** @module utils/storage */
2 |
3 | const items = new WeakMap();
4 | const { hasOwnProperty } = Object.prototype;
5 | const has = (obj, key) => hasOwnProperty.call(obj, key);
6 |
7 | /**
8 | * Interface for key/ value store.
9 | * @interface Storage
10 | */
11 |
12 | /**
13 | * Get value at the given key.
14 | * @function
15 | * @name Storage#get
16 | * @param {string} key
17 | * @returns {Promise}
18 | */
19 |
20 | /**
21 | * Set value at the given key.
22 | * @function
23 | * @name Storage#set
24 | * @param {string} key
25 | * @param {any} value
26 | * @returns {Promise}
27 | */
28 |
29 | /**
30 | * Delete value at the given key.
31 | * @function
32 | * @name Storage#del
33 | * @param {string} key
34 | * @returns {Promise}
35 | */
36 |
37 | /**
38 | * Implements a trivial in-memory-storage that can be used
39 | * as a fallback if no other storage is available.
40 | * @private
41 | * @class
42 | * @implements {Storage}
43 | */
44 | class Storage {
45 | constructor() {
46 | items.set(this, {});
47 | }
48 |
49 | set(key, value) {
50 | items.get(this)[key] = value;
51 | return Promise.resolve();
52 | }
53 |
54 | del(key) {
55 | delete items.get(this)[key];
56 | return Promise.resolve();
57 | }
58 |
59 | get(key) {
60 | const obj = items.get(this);
61 | if (!has(obj, key)) {
62 | return Promise.reject(new Error(`No such key: ${key}`));
63 | }
64 | return Promise.resolve(obj[key]);
65 | }
66 | }
67 |
68 | export default Storage;
69 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/carefullyMapValues.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import mapValues from 'lodash/mapValues';
3 |
4 | const defaultIsEqual = (a, b) => a === b;
5 |
6 | /**
7 | * Like lodash/mapValues, but with more caution, e.g. when new value is the
8 | * the same as the old one, do not create a new object. Also gives ability
9 | * to remove selected fields.
10 | * @param {object} object to map
11 | * @param {function} mapValue
12 | * @param {function} isEqual
13 | * @returns {object}
14 | */
15 | const carefullyMapValues = (object, mapValue, isEqual = defaultIsEqual) => {
16 | let modified = false;
17 |
18 | const toRemove = [];
19 | const remove = k => toRemove.push(k);
20 |
21 | const newObject = mapValues(object, (v, k) => {
22 | const newValue = mapValue(v, k, remove);
23 | if (isEqual(newValue, v)) {
24 | return v;
25 | }
26 | modified = true;
27 | return newValue;
28 | });
29 | if (toRemove.length > 0) {
30 | return omit(newObject, toRemove);
31 | }
32 | if (!modified) {
33 | return object;
34 | }
35 | return newObject;
36 | };
37 |
38 | export default carefullyMapValues;
39 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/carefullyMapValues.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import carefullyMapValues from './carefullyMapValues';
3 |
4 | // TODO: Implement more tests.
5 | describe('Test utility - carefullyMapValues', () => {
6 | test('should remove and map at the same time', () => {
7 | expect(carefullyMapValues({
8 | 1: 1,
9 | 2: 2,
10 | 3: 3,
11 | 4: 4,
12 | }, (value, key, remove) => {
13 | if (value % 2 === 0) {
14 | return remove(key);
15 | }
16 | return value + 1;
17 | })).toEqual({
18 | 1: 2,
19 | 3: 4,
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/createDelayedTask.js:
--------------------------------------------------------------------------------
1 | const constant = x => () => x;
2 |
3 | /**
4 | * Create a function that will be executed with some delay
5 | * and can be canceled in the meatime.
6 | * @param {function} execute
7 | * @param {object} options
8 | * @param {function} options.getTimeout
9 | * @returns {function}
10 | */
11 | const createDelayedTask = (execute, {
12 | getTimeout = constant(1000),
13 | } = {}) => {
14 | const timeouts = {};
15 | /**
16 | * Scheudle task for the given element.
17 | * @param {string} id
18 | */
19 | const schedule = (id) => {
20 | if (timeouts[id]) {
21 | clearTimeout(timeouts[id]);
22 | }
23 | timeouts[id] = setTimeout(() => {
24 | execute(id);
25 | delete timeouts[id];
26 | }, getTimeout(id));
27 | };
28 | /**
29 | * Cancel task for the given element.
30 | * @param {string} id
31 | */
32 | const cancel = (id) => {
33 | if (timeouts[id]) {
34 | clearTimeout(timeouts[id]);
35 | delete timeouts[id];
36 | }
37 | };
38 |
39 | schedule.cancel = cancel;
40 | return schedule;
41 | };
42 |
43 | export default createDelayedTask;
44 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/createInsertEntities.js:
--------------------------------------------------------------------------------
1 | import forEach from 'lodash/forEach';
2 | import without from 'lodash/without';
3 | import isEmpty from 'lodash/isEmpty';
4 |
5 | /**
6 | * Create a function that can be used for inseting document snapshots into ddp.collections.
7 | * @param {string} itemsFieldName
8 | * @param {string} orderFieldName
9 | */
10 | const createInsertEntities = (itemsFieldName, orderFieldName) => {
11 | const insertIntoCollection = (state, dataSourceId, docs) => {
12 | if (!docs || isEmpty(docs)) {
13 | return state;
14 | }
15 | const nextById = {
16 | ...state && state.nextById,
17 | };
18 | forEach(docs, (fields, docId) => {
19 | nextById[docId] = {
20 | ...nextById[docId],
21 | [itemsFieldName]: {
22 | ...nextById[docId] && nextById[docId][itemsFieldName],
23 | [dataSourceId]: fields,
24 | },
25 | [orderFieldName]: [
26 | ...without(nextById[docId] && nextById[docId][orderFieldName], dataSourceId),
27 | dataSourceId,
28 | ],
29 | };
30 | });
31 | return {
32 | ...state,
33 | nextById,
34 | needsUpdate: true,
35 | };
36 | };
37 |
38 | const insertEntities = (state, dataSourceId, entities) => {
39 | if (isEmpty(entities)) {
40 | return state;
41 | }
42 | const newState = {
43 | ...state,
44 | };
45 | forEach(entities, (docs, collection) => {
46 | newState[collection] = insertIntoCollection(newState[collection], dataSourceId, docs);
47 | });
48 | return newState;
49 | };
50 |
51 | return insertEntities;
52 | };
53 |
54 | export default createInsertEntities;
55 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/createRemoveEntities.js:
--------------------------------------------------------------------------------
1 | import omit from 'lodash/omit';
2 | import without from 'lodash/without';
3 | import isEmpty from 'lodash/isEmpty';
4 | import carefullyMapValues from './carefullyMapValues';
5 |
6 | /**
7 | * Create a function that can be used for removing document snapshots
8 | * from ddp.collections.
9 | * @param {string} itemsFieldName
10 | * @param {string} orderFieldName
11 | */
12 | const createRemoveEntities = (itemsFieldName, orderFieldName) => {
13 | const removeFromCollection = (state, dataSourceId, docs) => {
14 | if (!docs) {
15 | return state;
16 | }
17 | const nextById = carefullyMapValues(state.nextById, (item, id, remove) => {
18 | if (!docs[id]) {
19 | return item;
20 | }
21 | const {
22 | [itemsFieldName]: items,
23 | [orderFieldName]: order,
24 | ...other
25 | } = item;
26 | if (!items) {
27 | return item;
28 | }
29 | const newItemsOrder = without(order, dataSourceId);
30 | if (newItemsOrder.length === 0) {
31 | if (isEmpty(other)) {
32 | return remove(id);
33 | }
34 | return other;
35 | }
36 | return {
37 | ...other,
38 | [itemsFieldName]: omit(items, dataSourceId),
39 | [orderFieldName]: newItemsOrder,
40 | };
41 | });
42 | return {
43 | ...state,
44 | nextById,
45 | needsUpdate: true,
46 | };
47 | };
48 |
49 | const removeEntities = (state, dataSourceId, entities) => {
50 | if (!entities || isEmpty(entities)) {
51 | return state;
52 | }
53 | return carefullyMapValues(state, (collection, name) =>
54 | removeFromCollection(collection, dataSourceId, entities[name]));
55 | };
56 |
57 | return removeEntities;
58 | };
59 |
60 | export default createRemoveEntities;
61 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/createValuesMappingSelector.js:
--------------------------------------------------------------------------------
1 | import { defaultMemoize } from 'reselect';
2 | import memoizeValuesMapping from './memoizeValuesMapping';
3 |
4 | const defaultIsEqual = (a, b) => a === b;
5 |
6 | const createValuesMappingSelector = (selectObject, mapOneValue, isEqual = defaultIsEqual) => {
7 | let recomputations = 0;
8 | const memoizedMapValues = memoizeValuesMapping((value, key) => {
9 | recomputations += 1;
10 | return mapOneValue(value, key);
11 | }, isEqual);
12 | const selector = defaultMemoize((...args) => memoizedMapValues(selectObject(...args)));
13 | selector.recomputations = () => recomputations;
14 | selector.resetRecomputations = () => {
15 | recomputations = 0;
16 | };
17 | return selector;
18 | };
19 |
20 | export default createValuesMappingSelector;
21 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/createValuesMappingSelector.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import createValuesMappingSelector from './createValuesMappingSelector';
4 |
5 | const constant = x => () => x;
6 | const identity = x => x;
7 |
8 | describe('Test utility - createValuesMappingSelector', () => {
9 | let testContext;
10 |
11 | beforeEach(() => {
12 | testContext = {};
13 | });
14 |
15 | beforeEach(() => {
16 | testContext.object = {};
17 | testContext.identity = createValuesMappingSelector(identity, identity);
18 | testContext.constant = createValuesMappingSelector(identity, constant(testContext.object));
19 | });
20 |
21 | describe('Given an empty object', () => {
22 | test('should not be changed by identity mapping', () => {
23 | const x = {};
24 | expect(testContext.identity(x)).toBe(x);
25 | });
26 |
27 | test('should not be changed by constant mapping', () => {
28 | const x = {};
29 | expect(testContext.constant(x)).toBe(x);
30 | });
31 |
32 | test(
33 | 'should return the same result when called with similar argument',
34 | () => {
35 | const x = {};
36 | const y = {};
37 | expect(testContext.constant(x)).toBe(testContext.constant(y));
38 | },
39 | );
40 | });
41 |
42 | describe('Given a non-empty object', () => {
43 | test('should not be changed by identity mapping', () => {
44 | const x = {
45 | a: {},
46 | b: {},
47 | };
48 | expect(testContext.identity(x)).toBe(x);
49 | });
50 | test('should be changed by constant mapping', () => {
51 | const x = {
52 | a: {},
53 | b: {},
54 | };
55 | expect(testContext.constant(x)).not.toBe(x);
56 | });
57 | test(
58 | 'should return the same result when called with similar argument',
59 | () => {
60 | const x = {
61 | a: {},
62 | b: {},
63 | };
64 | const y = {
65 | ...x,
66 | };
67 | expect(testContext.constant(x)).toBe(testContext.constant(y));
68 | },
69 | );
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/memoizeValuesMapping.js:
--------------------------------------------------------------------------------
1 | import shallowEqual from 'shallowequal';
2 | import carefullyMapValues from './carefullyMapValues';
3 |
4 | const defaultIsEqual = (a, b) => a === b;
5 |
6 | const memoizeValuesMapping = (mapOneValue, isEqual = defaultIsEqual) => {
7 | let lastInput = null;
8 | let lastResult = null;
9 | return (input) => {
10 | if (!lastResult) {
11 | lastResult = input;
12 | }
13 | const result = carefullyMapValues(input, (value, key) => {
14 | const lastValue = lastResult && lastResult[key];
15 | if (lastInput && lastInput[key] === value) {
16 | return lastValue;
17 | }
18 | return mapOneValue(value, key);
19 | }, isEqual);
20 | lastInput = input;
21 | if (!shallowEqual(result, lastResult)) {
22 | lastResult = result;
23 | }
24 | return lastResult;
25 | };
26 | };
27 |
28 | export default memoizeValuesMapping;
29 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/memoizeValuesMapping.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 | import memoizeValuesMapping from './memoizeValuesMapping';
3 |
4 | const constant = x => () => x;
5 | const identity = x => x;
6 |
7 | describe('Test utility - memoizeValuesMapping', () => {
8 | let testContext;
9 |
10 | beforeEach(() => {
11 | testContext = {};
12 | });
13 |
14 | beforeEach(() => {
15 | testContext.object = {};
16 | testContext.identity = memoizeValuesMapping(identity);
17 | testContext.constant = memoizeValuesMapping(constant(testContext.object));
18 | });
19 |
20 | describe('Given an empty object', () => {
21 | test('should not be changed by identity mapping', () => {
22 | const x = {};
23 | expect(testContext.identity(x)).toBe(x);
24 | });
25 |
26 | test('should not be changed by constant mapping', () => {
27 | const x = {};
28 | expect(testContext.constant(x)).toBe(x);
29 | });
30 |
31 | test(
32 | 'should return the same result when called with similar argument',
33 | () => {
34 | const x = {};
35 | const y = {};
36 | expect(testContext.constant(x)).toBe(testContext.constant(y));
37 | },
38 | );
39 | });
40 |
41 | describe('Given a non-empty object', () => {
42 | test('should not be changed by identity mapping', () => {
43 | const x = {
44 | a: {},
45 | b: {},
46 | };
47 | expect(testContext.identity(x)).toBe(x);
48 | });
49 | test('should be changed by constant mapping', () => {
50 | const x = {
51 | a: {},
52 | b: {},
53 | };
54 | expect(testContext.constant(x)).not.toBe(x);
55 | });
56 | test(
57 | 'should return the same result when called with similar argument',
58 | () => {
59 | const x = {
60 | a: {},
61 | b: {},
62 | };
63 | const y = {
64 | ...x,
65 | };
66 | expect(testContext.constant(x)).toBe(testContext.constant(y));
67 | },
68 | );
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/sha256.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | *
5 | * Secure Hash Algorithm (SHA256)
6 | * http://www.webtoolkit.info/javascript-sha256.html
7 | * http://anmar.eu.org/projects/jssha2/
8 | *
9 | * Original code by Angel Marin, Paul Johnston.
10 | *
11 | **/
12 |
13 | function SHA256(s) {
14 |
15 | var chrsz = 8;
16 | var hexcase = 0;
17 |
18 | function safe_add (x, y) {
19 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
20 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
21 | return (msw << 16) | (lsw & 0xFFFF);
22 | }
23 |
24 | function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
25 | function R (X, n) { return ( X >>> n ); }
26 | function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
27 | function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
28 | function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
29 | function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
30 | function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
31 | function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
32 |
33 | function core_sha256 (m, l) {
34 | var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
35 | var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
36 | var W = new Array(64);
37 | var a, b, c, d, e, f, g, h, i, j;
38 | var T1, T2;
39 |
40 | m[l >> 5] |= 0x80 << (24 - l % 32);
41 | m[((l + 64 >> 9) << 4) + 15] = l;
42 |
43 | for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
87 | }
88 | return bin;
89 | }
90 |
91 | function Utf8Encode(string) {
92 | // METEOR change:
93 | // The webtoolkit.info version of this code added this
94 | // Utf8Encode function (which does seem necessary for dealing
95 | // with arbitrary Unicode), but the following line seems
96 | // problematic:
97 | //
98 | // string = string.replace(/\r\n/g,"\n");
99 | var utftext = "";
100 |
101 | for (var n = 0; n < string.length; n++) {
102 |
103 | var c = string.charCodeAt(n);
104 |
105 | if (c < 128) {
106 | utftext += String.fromCharCode(c);
107 | }
108 | else if((c > 127) && (c < 2048)) {
109 | utftext += String.fromCharCode((c >> 6) | 192);
110 | utftext += String.fromCharCode((c & 63) | 128);
111 | }
112 | else {
113 | utftext += String.fromCharCode((c >> 12) | 224);
114 | utftext += String.fromCharCode(((c >> 6) & 63) | 128);
115 | utftext += String.fromCharCode((c & 63) | 128);
116 | }
117 |
118 | }
119 |
120 | return utftext;
121 | }
122 |
123 | function binb2hex (binarray) {
124 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
125 | var str = "";
126 | for(var i = 0; i < binarray.length * 4; i++) {
127 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
128 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
129 | }
130 | return str;
131 | }
132 |
133 | s = Utf8Encode(s);
134 | return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
135 | }
136 |
137 | export default SHA256;
138 |
--------------------------------------------------------------------------------
/ddp-redux/src/utils/stableMap.js:
--------------------------------------------------------------------------------
1 | import map from 'lodash/map';
2 | import mapValues from 'lodash/mapValues';
3 | import isArray from 'lodash/isArray';
4 |
5 | const stableMap = (collection, ...args) => (isArray(collection)
6 | ? map(collection, ...args)
7 | : mapValues(collection, ...args)
8 | );
9 |
10 | export default stableMap;
11 |
--------------------------------------------------------------------------------
/example/.tmux.conf:
--------------------------------------------------------------------------------
1 | # Configuration for tmux 2.1
2 |
3 | set -g mouse on
4 |
5 | bind -t vi-copy WheelUpPane halfpage-up
6 | bind -t vi-copy WheelDownPane halfpage-down
7 |
8 | # Copy & Paste
9 |
10 | setw -g mode-keys vi
11 |
12 | # Setup 'v' to begin selection as in Vim
13 | bind-key -t vi-copy v begin-selection
14 | bind-key -t vi-copy y copy-pipe "reattach-to-user-namespace pbcopy"
15 |
16 | # Update default binding of `Enter` to also use copy-pipe
17 | unbind -t vi-copy Enter
18 | bind-key -t vi-copy Enter copy-pipe "reattach-to-user-namespace pbcopy"
19 |
--------------------------------------------------------------------------------
/example/backend/.eslintrc.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var packages = path.resolve(__dirname, '.meteor', 'packages');
4 |
5 | module.exports = {
6 | settings: {
7 | 'import/resolver': 'meteor',
8 | 'import/core-modules': fs.readFileSync(packages, 'utf-8')
9 | .split('\n')
10 | .filter(name => name.charAt(0) !=='#')
11 | .filter(name => name.length > 0)
12 | .map(name => name.indexOf('@') > -1 ? name.split('@')[0] : name)
13 | .map(name => 'meteor/' + name)
14 | .concat(['meteor/meteor'])
15 | },
16 | };
--------------------------------------------------------------------------------
/example/backend/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 |
--------------------------------------------------------------------------------
/example/backend/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 | 1.4.3-split-account-service-packages
17 | 1.5-add-dynamic-import-package
18 |
--------------------------------------------------------------------------------
/example/backend/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/example/backend/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | 57jic3xl3ucqmaaveg
8 |
--------------------------------------------------------------------------------
/example/backend/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base@1.2.0 # Packages every Meteor app needs to have
8 | mobile-experience@1.0.5 # Packages for a great mobile UX
9 | mongo@1.3.1 # The database Meteor supports right now
10 | blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
11 | reactive-var@1.0.11 # Reactive variable for tracker
12 | tracker@1.1.3 # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-css@1.3.5 # CSS minifier run for production mode
15 | standard-minifier-js@2.2.0 # JS minifier run for production mode
16 | es5-shim@4.6.15 # ECMAScript 5 compatibility for older browsers.
17 | ecmascript@0.9.0 # Enable ECMAScript2015+ syntax in app code
18 | shell-server@0.3.0 # Server-side component of the `meteor shell` command
19 |
20 | insecure@1.0.7 # Allow all DB writes from clients (for prototyping)
21 | mdg:validation-error
22 | mdg:validated-method
23 | aldeed:simple-schema
24 | aldeed:collection2
25 | dynamic-import@0.2.0
26 | accounts-base
27 | accounts-password
28 | meteorhacks:aggregate
29 |
--------------------------------------------------------------------------------
/example/backend/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/example/backend/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@1.6.0.1
2 |
--------------------------------------------------------------------------------
/example/backend/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.4.0
2 | accounts-password@1.5.0
3 | aldeed:collection2@2.10.0
4 | aldeed:collection2-core@1.2.0
5 | aldeed:schema-deny@1.1.0
6 | aldeed:schema-index@1.1.1
7 | aldeed:simple-schema@1.5.3
8 | allow-deny@1.1.0
9 | autoupdate@1.3.12
10 | babel-compiler@6.24.7
11 | babel-runtime@1.1.1
12 | base64@1.0.10
13 | binary-heap@1.0.10
14 | blaze@2.3.2
15 | blaze-html-templates@1.1.2
16 | blaze-tools@1.0.10
17 | boilerplate-generator@1.3.1
18 | caching-compiler@1.1.9
19 | caching-html-compiler@1.1.2
20 | callback-hook@1.0.10
21 | check@1.2.5
22 | ddp@1.4.0
23 | ddp-client@2.2.0
24 | ddp-common@1.3.0
25 | ddp-rate-limiter@1.0.7
26 | ddp-server@2.1.1
27 | deps@1.0.12
28 | diff-sequence@1.0.7
29 | dynamic-import@0.2.1
30 | ecmascript@0.9.0
31 | ecmascript-runtime@0.5.0
32 | ecmascript-runtime-client@0.5.0
33 | ecmascript-runtime-server@0.5.0
34 | ejson@1.1.0
35 | email@1.2.3
36 | es5-shim@4.6.15
37 | geojson-utils@1.0.10
38 | hot-code-push@1.0.4
39 | html-tools@1.0.11
40 | htmljs@1.0.11
41 | http@1.3.0
42 | id-map@1.0.9
43 | insecure@1.0.7
44 | jquery@1.11.10
45 | launch-screen@1.1.1
46 | livedata@1.0.18
47 | localstorage@1.2.0
48 | logging@1.1.19
49 | mdg:validated-method@1.1.0
50 | mdg:validation-error@0.5.1
51 | meteor@1.8.2
52 | meteor-base@1.2.0
53 | meteorhacks:aggregate@1.3.0
54 | meteorhacks:collection-utils@1.2.0
55 | minifier-css@1.2.16
56 | minifier-js@2.2.2
57 | minimongo@1.4.3
58 | mobile-experience@1.0.5
59 | mobile-status-bar@1.0.14
60 | modules@0.11.1
61 | modules-runtime@0.9.1
62 | mongo@1.3.1
63 | mongo-dev-server@1.1.0
64 | mongo-id@1.0.6
65 | mongo-livedata@1.0.12
66 | npm-bcrypt@0.9.3
67 | npm-mongo@2.2.33
68 | observe-sequence@1.0.16
69 | ordered-dict@1.0.9
70 | promise@0.10.0
71 | raix:eventemitter@0.1.3
72 | random@1.0.10
73 | rate-limit@1.0.8
74 | reactive-var@1.0.11
75 | reload@1.1.11
76 | retry@1.0.9
77 | routepolicy@1.0.12
78 | service-configuration@1.0.11
79 | sha@1.0.9
80 | shell-server@0.3.1
81 | spacebars@1.0.15
82 | spacebars-compiler@1.1.3
83 | srp@1.0.10
84 | standard-minifier-css@1.3.5
85 | standard-minifier-js@2.2.3
86 | templating@1.3.2
87 | templating-compiler@1.3.3
88 | templating-runtime@1.3.2
89 | templating-tools@1.1.2
90 | tracker@1.1.3
91 | ui@1.0.13
92 | underscore@1.0.10
93 | url@1.1.0
94 | webapp@1.4.0
95 | webapp-hashing@1.0.9
96 |
--------------------------------------------------------------------------------
/example/backend/imports/api/TodoLists.js:
--------------------------------------------------------------------------------
1 | import TodoLists from '/imports/collections/TodoLists';
2 | import * as api from '/imports/common/api/TodoLists';
3 | import publish from './publish';
4 | import implement from './implement';
5 |
6 | implement(api.insert, {
7 | run({
8 | title,
9 | }) {
10 | return TodoLists.insert({
11 | title,
12 | userId: this.userId,
13 | });
14 | },
15 | });
16 |
17 | implement(api.update, {
18 | run({
19 | listId,
20 | title,
21 | }) {
22 | return TodoLists.update({
23 | _id: listId,
24 | userId: this.userId,
25 | }, {
26 | $set: {
27 | title,
28 | },
29 | });
30 | },
31 | });
32 |
33 | implement(api.remove, {
34 | run({
35 | listId,
36 | }) {
37 | return TodoLists.remove({
38 | _id: listId,
39 | userId: this.userId,
40 | });
41 | },
42 | });
43 |
44 | publish(api.allLists, {
45 | run() {
46 | return TodoLists.find({
47 | userId: this.userId,
48 | });
49 | },
50 | });
51 |
52 | publish(api.oneList, {
53 | run({ listId }) {
54 | return TodoLists.find({
55 | _id: listId,
56 | userId: this.userId,
57 | });
58 | },
59 | });
60 |
61 |
--------------------------------------------------------------------------------
/example/backend/imports/api/Todos.js:
--------------------------------------------------------------------------------
1 | import Todos from '/imports/collections/Todos';
2 | import TodoList from '/imports/common/models/TodoList';
3 | import * as api from '/imports/common/api/Todos';
4 | import publish from './publish';
5 | import implement from './implement';
6 |
7 | implement(api.insert, {
8 | run({
9 | name,
10 | done,
11 | listId,
12 | }) {
13 | Todos.insert({
14 | name,
15 | done,
16 | listId,
17 | userId: this.userId,
18 | });
19 | },
20 | });
21 |
22 | implement(api.update, {
23 | run({
24 | todoId,
25 | name,
26 | done,
27 | }) {
28 | const mutation = {
29 | $set: { name },
30 | };
31 | if (!done) {
32 | mutation.$unset = { done: 1 };
33 | } else {
34 | mutation.$set.done = true;
35 | }
36 | Todos.update({
37 | _id: todoId,
38 | userId: this.userId,
39 | }, mutation);
40 | },
41 | });
42 |
43 | implement(api.remove, {
44 | run({
45 | todoId,
46 | }) {
47 | Todos.remove({
48 | _id: todoId,
49 | userId: this.userId,
50 | });
51 | },
52 | });
53 |
54 | publish(api.todosInList, {
55 | run({ listId }) {
56 | return Todos.find({
57 | listId,
58 | userId: this.userId,
59 | });
60 | },
61 | });
62 |
63 | implement(api.getStats, {
64 | run() {
65 | const results = Todos.aggregate([
66 | { $match: {
67 | userId: this.userId,
68 | } },
69 | { $group: {
70 | _id: {
71 | listId: '$listId',
72 | state: {
73 | $cond: {
74 | if: '$done',
75 | then: 'completed',
76 | else: 'active',
77 | },
78 | },
79 | },
80 | count: { $sum: 1 },
81 | } },
82 | ]);
83 | const byListId = {};
84 | results.forEach(({ _id: { listId, state }, count }) => {
85 | if (!byListId[listId]) {
86 | byListId[listId] = {};
87 | }
88 | byListId[listId][state] = count;
89 | });
90 | return {
91 | entities: {
92 | [TodoList.collection]: byListId,
93 | },
94 | };
95 | },
96 | });
97 |
--------------------------------------------------------------------------------
/example/backend/imports/api/implement.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { ValidatedMethod } from 'meteor/mdg:validated-method';
3 | import { ValidationError } from 'meteor/mdg:validation-error';
4 |
5 | function errors(methodOptions) {
6 | const originalRun = methodOptions.run;
7 | return {
8 | ...methodOptions,
9 | run(...args) {
10 | this.error = function (error, reason, details) {
11 | return new Meteor.Error(`${methodOptions.name}.${error}`, reason, details);
12 | };
13 | return originalRun.call(this, ...args);
14 | },
15 | };
16 | }
17 |
18 | const implement = (method, { mixins = [], onServer, ...options } = {}) => {
19 | const adjustedOptions = { ...options };
20 | const name = method.getName();
21 | if (onServer) {
22 | let func = () => {};
23 | if (Meteor.isServer) {
24 | func = onServer();
25 | }
26 | if (!func) {
27 | throw new Error(`Invalid method definition, check "onServer" for ${method.getName()}`);
28 | }
29 | if (options.run) {
30 | throw new Error('When "onServer" is provided, "run" is not allowed');
31 | }
32 | adjustedOptions.run = function (...args) {
33 | if (!this.userId) {
34 | throw new Meteor.Error(`${name}.notAllowed`, 'You must be logged in');
35 | }
36 | return func(this, ...args);
37 | };
38 | }
39 | return new ValidatedMethod({
40 | name,
41 | validate: method.getValidator(ValidationError),
42 | mixins: [
43 | ...mixins,
44 | errors,
45 | ],
46 | ...adjustedOptions,
47 | });
48 | };
49 |
50 | export default implement;
51 |
--------------------------------------------------------------------------------
/example/backend/imports/api/index.js:
--------------------------------------------------------------------------------
1 | import './TodoLists.js';
2 | import './Todos.js';
3 |
--------------------------------------------------------------------------------
/example/backend/imports/api/publish.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { ValidationError } from 'meteor/mdg:validation-error';
3 |
4 | const publish = (apiSpec, { run }) => {
5 | const name = apiSpec.getName();
6 | const validate = apiSpec.getValidator(ValidationError);
7 | Meteor.publish(name, function (params) {
8 | validate(params);
9 | return run.call(this, params);
10 | });
11 | };
12 |
13 | export default publish;
14 |
--------------------------------------------------------------------------------
/example/backend/imports/collections/TodoLists.js:
--------------------------------------------------------------------------------
1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 | import { Mongo } from 'meteor/mongo';
3 | import CreatedUpdatedSchema from '../schema/CreatedUpdatedSchema';
4 | import TodoList from '../common/models/TodoList';
5 |
6 | const TodoLists = new Mongo.Collection(TodoList.collection, {
7 | transform: doc => new TodoList(doc),
8 | });
9 |
10 | TodoLists.schema = new SimpleSchema([CreatedUpdatedSchema, {
11 | userId: { type: String, regEx: SimpleSchema.RegEx.Id },
12 | title: { type: String },
13 | }]);
14 |
15 | TodoLists.attachSchema(TodoLists.schema);
16 |
17 | export default TodoLists;
18 |
--------------------------------------------------------------------------------
/example/backend/imports/collections/Todos.js:
--------------------------------------------------------------------------------
1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 | import { Mongo } from 'meteor/mongo';
3 | import CreatedUpdatedSchema from '../schema/CreatedUpdatedSchema';
4 | import Todo from '../common/models/Todo';
5 |
6 | const Todos = new Mongo.Collection(Todo.collection, {
7 | transform: doc => new Todo(doc),
8 | });
9 |
10 | Todos.schema = new SimpleSchema([CreatedUpdatedSchema, {
11 | userId: { type: String, regEx: SimpleSchema.RegEx.Id },
12 | listId: { type: String, regEx: SimpleSchema.RegEx.Id },
13 | name: { type: String },
14 | done: { type: Boolean, optional: true },
15 | }]);
16 |
17 | Todos.attachSchema(Todos.schema);
18 |
19 | export default Todos;
20 |
--------------------------------------------------------------------------------
/example/backend/imports/common:
--------------------------------------------------------------------------------
1 | ../../todo-web/src/common
--------------------------------------------------------------------------------
/example/backend/imports/schema/CreatedUpdatedSchema.js:
--------------------------------------------------------------------------------
1 | import { SimpleSchema } from 'meteor/aldeed:simple-schema';
2 | import {
3 | setDateOnInsert,
4 | setUserOnInsert,
5 | setDateOnUpdate,
6 | setUserOnUpdate,
7 | } from './autoValue.js';
8 |
9 | const CreatedUpdatedSchema = new SimpleSchema({
10 |
11 | createdAt: { type: Date, autoValue: setDateOnInsert },
12 | createdBy: { type: String, optional: true, regEx: SimpleSchema.RegEx.Id, autoValue: setUserOnInsert },
13 |
14 | updatedAt: { type: Date, optional: true, autoValue: setDateOnUpdate },
15 | updatedBy: { type: String, optional: true, regEx: SimpleSchema.RegEx.Id, autoValue: setUserOnUpdate },
16 |
17 | });
18 |
19 | export default CreatedUpdatedSchema;
20 |
--------------------------------------------------------------------------------
/example/backend/imports/schema/autoValue.js:
--------------------------------------------------------------------------------
1 | export function setUserOnInsert() {
2 | if (!this.isSet && !this.isUpdate) {
3 | return this.userId;
4 | }
5 | return undefined;
6 | }
7 |
8 | export function setDateOnInsert() {
9 | if (!this.isSet && !this.isUpdate) {
10 | return new Date();
11 | }
12 | return undefined;
13 | }
14 |
15 | export function setUserOnUpdate() {
16 | if (this.isUpdate || this.isUpsert) {
17 | return this.userId;
18 | }
19 | return undefined;
20 | }
21 |
22 | export function setDateOnUpdate() {
23 | if (this.isUpdate || this.isUpsert) {
24 | return new Date();
25 | }
26 | return undefined;
27 | }
28 |
--------------------------------------------------------------------------------
/example/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run"
6 | },
7 | "dependencies": {
8 | "babel-runtime": "^6.25.0",
9 | "bcrypt": "^1.0.3",
10 | "ddp-redux": "file:../../ddp-redux",
11 | "flat": "^2.0.1",
12 | "lodash": "^4.17.4",
13 | "meteor-node-stubs": "~0.2.4",
14 | "react": "^15.6.1",
15 | "reselect": "^3.0.1",
16 | "very-simple-schema": "0.0.8"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/backend/server/main.js:
--------------------------------------------------------------------------------
1 | import '/imports/api';
2 |
--------------------------------------------------------------------------------
/example/start-develop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | tmux new-session -d -n "example" -s example
3 | tmux source-file .tmux.conf
4 | tmux new-window -n "backend" -t example:1
5 | tmux new-window -n "todo-web" -t example:2
6 | tmux new-window -n "ddp-redux" -t example:3
7 | tmux new-window -n "ddp-connector" -t example:4
8 | tmux send-keys -t example:1 "cd backend" C-m
9 | tmux send-keys -t example:1 "meteor npm i" C-m
10 | tmux send-keys -t example:1 "meteor --port 4000" C-m
11 | tmux send-keys -t example:2 "cd todo-web" C-m
12 | tmux send-keys -t example:2 "npm install" C-m
13 | tmux send-keys -t example:2 "npm run start" C-m
14 | tmux send-keys -t example:3 "cd ../ddp-redux" C-m
15 | tmux send-keys -t example:3 "npm install" C-m
16 | tmux send-keys -t example:3 "npm run build-watch" C-m
17 | tmux send-keys -t example:4 "cd ../ddp-connector" C-m
18 | tmux send-keys -t example:4 "npm install" C-m
19 | tmux send-keys -t example:4 "npm run build-watch" C-m
20 | tmux attach-session -d
21 |
--------------------------------------------------------------------------------
/example/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | (
3 | cd ../ddp-connector
4 | npm install
5 | npm run build
6 | )
7 | (
8 | cd ../ddp-redux
9 | npm install
10 | npm run build
11 | )
12 | tmux new-session -d -n "example" -s example
13 | tmux source-file .tmux.conf
14 | tmux new-window -n "backend" -t example:1
15 | tmux new-window -n "todo-web" -t example:2
16 | tmux send-keys -t example:1 "cd backend" C-m
17 | tmux send-keys -t example:1 "meteor npm i" C-m
18 | tmux send-keys -t example:1 "meteor --port 4000" C-m
19 | tmux send-keys -t example:2 "cd todo-web" C-m
20 | tmux send-keys -t example:2 "npm install" C-m
21 | tmux send-keys -t example:2 "npm run start" C-m
22 | tmux attach-session -d
23 |
--------------------------------------------------------------------------------
/example/todo-web/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_ENDPOINT=ws://localhost:4000/websocket
2 |
--------------------------------------------------------------------------------
/example/todo-web/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "import/prefer-default-export": "off",
5 | "react/jsx-filename-extension": "off"
6 | },
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaVersion": 7,
10 | "sourceType": "module",
11 | "ecmaFeatures": {
12 | "experimentalObjectRestSpread": true
13 | }
14 | },
15 | "env": {
16 | "es6": true,
17 | "browser": true,
18 | "node": true
19 | },
20 | "plugins": [],
21 | "settings": {}
22 | }
23 |
--------------------------------------------------------------------------------
/example/todo-web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules/*
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/example/todo-web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "antd": "^3.0.2",
7 | "ddp-connector": "file:../../ddp-connector",
8 | "ddp-redux": "file:../../ddp-redux",
9 | "eslint": "^4.3.0",
10 | "eslint-config-airbnb": "^15.1.0",
11 | "eslint-plugin-import": "^2.7.0",
12 | "eslint-plugin-jsx-a11y": "^5.1.1",
13 | "eslint-plugin-react": "^7.1.0",
14 | "final-form": "^3.0.0",
15 | "flat": "^4.0.0",
16 | "lodash.some": "^4.6.0",
17 | "react": "^16.2.0",
18 | "react-dom": "^16.2.0",
19 | "react-final-form": "^2.0.0",
20 | "react-redux": "^5.0.6",
21 | "react-router": "^4.2.0",
22 | "react-router-dom": "^4.2.2",
23 | "react-scripts": "1.0.17",
24 | "recompose": "^0.26.0",
25 | "redux": "^3.7.2",
26 | "redux-devtools": "^3.4.0",
27 | "redux-thunk": "^2.2.0",
28 | "reselect": "^3.0.1",
29 | "very-simple-schema": "0.0.8"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test --env=jsdom",
35 | "eject": "react-scripts eject"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/example/todo-web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apendua/ddp-redux/412b4e38de09d398ad7bcc964f920f6fcf907396/example/todo-web/public/favicon.ico
--------------------------------------------------------------------------------
/example/todo-web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | Todos Example
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/example/todo-web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | !/node_modules/ddp-connector
3 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/api/TodoLists.js:
--------------------------------------------------------------------------------
1 | import Schema from '../utils/Schema.js';
2 | import ApiSpec from '../utils/ApiSpec.js';
3 |
4 | export const insert = new ApiSpec({
5 | name: 'api.TodoLists.insert',
6 | schema: new Schema({
7 | title: { type: String },
8 | }),
9 | });
10 |
11 | export const update = new ApiSpec({
12 | name: 'api.TodoLists.update',
13 | schema: new Schema({
14 | listId: { type: String, regEx: Schema.RegEx.Id },
15 | title: { type: String, optional: true },
16 | }),
17 | });
18 |
19 | export const remove = new ApiSpec({
20 | name: 'api.TodoLists.remove',
21 | schema: new Schema({
22 | listId: { type: String, regEx: Schema.RegEx.Id },
23 | }),
24 | });
25 |
26 | export const allLists = new ApiSpec({
27 | name: 'api.TodoLists.allLists',
28 | schema: new Schema(),
29 | });
30 |
31 | export const oneList = new ApiSpec({
32 | name: 'api.TodoLists.oneList',
33 | schema: new Schema({
34 | listId: { type: String, regEx: Schema.RegEx.Id },
35 | }),
36 | });
37 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/api/Todos.js:
--------------------------------------------------------------------------------
1 | import Schema from '../utils/Schema.js';
2 | import ApiSpec from '../utils/ApiSpec.js';
3 |
4 | export const insert = new ApiSpec({
5 | name: 'api.Todos.insert',
6 | schema: new Schema({
7 | listId: { type: String, regEx: Schema.RegEx.Id },
8 | name: { type: String },
9 | done: { type: Boolean, optional: true },
10 | }),
11 | });
12 |
13 | export const update = new ApiSpec({
14 | name: 'api.Todos.update',
15 | schema: new Schema({
16 | todoId: { type: String, regEx: Schema.RegEx.Id },
17 | name: { type: String, optional: true },
18 | done: { type: Boolean, optional: true },
19 | }),
20 | });
21 |
22 | export const remove = new ApiSpec({
23 | name: 'api.Todos.remove',
24 | schema: new Schema({
25 | todoId: { type: String, regEx: Schema.RegEx.Id },
26 | }),
27 | });
28 |
29 | export const todosInList = new ApiSpec({
30 | name: 'api.Todos.todosInList',
31 | schema: new Schema({
32 | listId: { type: String, regEx: Schema.RegEx.Id },
33 | }),
34 | });
35 |
36 | export const getStats = new ApiSpec({
37 | name: 'api.Todos.getStats',
38 | schema: new Schema(),
39 | });
40 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/models/BaseModel.js:
--------------------------------------------------------------------------------
1 | class BaseModel {
2 | constructor(doc) {
3 | Object.assign(this, doc);
4 | }
5 |
6 | static set collection(name) {
7 | this._collection = name;
8 | }
9 |
10 | static get collection() {
11 | return this._collection;
12 | }
13 | }
14 |
15 | export default BaseModel;
16 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/models/Todo.js:
--------------------------------------------------------------------------------
1 | import BaseModel from './BaseModel.js';
2 |
3 | class Todo extends BaseModel {
4 | isDone() {
5 | return !!this.done;
6 | }
7 | getName() {
8 | return this.name;
9 | }
10 | getListId() {
11 | return this.listId;
12 | }
13 | }
14 |
15 | Todo.collection = 'Todos';
16 | export default Todo;
17 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/models/TodoList.js:
--------------------------------------------------------------------------------
1 | import BaseModel from './BaseModel.js';
2 |
3 | class TodoList extends BaseModel {
4 | getTitle() {
5 | return this.title;
6 | }
7 | }
8 |
9 | TodoList.collection = 'TodoLists';
10 | export default TodoList;
11 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/utils/ApiSpec.js:
--------------------------------------------------------------------------------
1 | import { makeSchemaValidator } from './errors.js';
2 |
3 | class ApiSpec {
4 | constructor(options) {
5 | Object.assign(this, options);
6 | if (!this.name) {
7 | throw new Error('Api spec requires name');
8 | }
9 | }
10 |
11 | getName() {
12 | return this.name;
13 | }
14 |
15 | withParams(params) {
16 | if (this.schema) {
17 | try {
18 | this.schema.validate(params); // this may throw an error!
19 | } catch (err) {
20 | console.error(err);
21 | console.error('GOT', params);
22 | throw err;
23 | }
24 | }
25 | return {
26 | name: this.getName(),
27 | params: params !== undefined ? [params] : [],
28 | };
29 | }
30 |
31 | getValidator(ValidationError) {
32 | return makeSchemaValidator(this.schema, ValidationError);
33 | }
34 | }
35 |
36 | export default ApiSpec;
37 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/utils/Schema.js:
--------------------------------------------------------------------------------
1 | import {
2 | createSchema,
3 | presetDefault,
4 | } from 'very-simple-schema';
5 |
6 | const Schema = createSchema({
7 | plugins: [
8 | ...presetDefault,
9 | ],
10 | additionalProperties: false,
11 | emptyStringsAreMissingValues: true,
12 | });
13 |
14 | export default Schema;
15 |
--------------------------------------------------------------------------------
/example/todo-web/src/common/utils/errors.js:
--------------------------------------------------------------------------------
1 | import flatten from 'flat';
2 |
3 | const getErrorMessage = (message) => {
4 | if (typeof message === 'string') {
5 | return message;
6 | }
7 | if (Array.isArray(message)) {
8 | return getErrorMessage(message[0]);
9 | }
10 | if (typeof message === 'object') {
11 | const key = Object.keys(message)[0];
12 | return getErrorMessage(message[key]);
13 | }
14 | return 'Unrecognized error';
15 | };
16 |
17 | export const makeSchemaValidator = (schema, ValidationError) => {
18 | if (!schema) {
19 | return () => {};
20 | }
21 | return (value) => {
22 | const error = schema.getErrors(value);
23 | if (error) {
24 | const errors = [];
25 | const described = schema.describe(error);
26 | const reason = getErrorMessage(described);
27 | if (typeof described === 'object') {
28 | const fields = flatten(described);
29 | Object.keys(fields).forEach((name) => {
30 | errors.push({
31 | name,
32 | type: fields[name],
33 | });
34 | });
35 | }
36 | throw new ValidationError(errors, reason);
37 | }
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/example/todo-web/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loader = () => (
4 | Loading ...
5 | );
6 |
7 | export default Loader;
8 |
--------------------------------------------------------------------------------
/example/todo-web/src/components/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NotFound = () => (
4 | Not found ...
5 | );
6 |
7 | export default NotFound;
--------------------------------------------------------------------------------
/example/todo-web/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Provider } from 'react-redux';
4 | import Router from '../routes/Router';
5 |
6 | const App = ({ store }) => (
7 |
8 |
9 |
10 | );
11 |
12 | App.propTypes = {
13 | store: PropTypes.object.isRequired,
14 | };
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/example/todo-web/src/containers/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/example/todo-web/src/containers/Entry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Form,
4 | Field,
5 | } from 'react-final-form';
6 | import AntdForm from 'antd/lib/form';
7 | import Input from 'antd/lib/input';
8 | import Button from 'antd/lib/button';
9 | import Checkbox from 'antd/lib/checkbox';
10 | import { Redirect } from 'react-router-dom';
11 | import ddp from 'ddp-connector';
12 | import {
13 | loginWithPassword,
14 | createUser,
15 | } from 'ddp-redux';
16 |
17 | // const composeValidators = (...validators) =>
18 | // validators.reudce((a, b) => value => a(value) || b(value))
19 |
20 | const required = value => (value ? undefined : 'Value is required');
21 | const FormItem = AntdForm.Item;
22 |
23 | const Entry = ddp({
24 | selectors: ({
25 | from,
26 | }) => ({
27 | user: from('users').select.currentUser(),
28 | }),
29 | mutations: {
30 | onSubmit: ({
31 | dispatch,
32 | }) => ({
33 | email,
34 | password,
35 | createNew,
36 | }) => (createNew
37 | ? dispatch(createUser({ email, password }))
38 | : dispatch(loginWithPassword({ email, password }))
39 | ),
40 | },
41 | })(({
42 | user,
43 | onSubmit,
44 | }) => (user
45 | ?
46 |
47 |
48 | :
109 | )}
110 | />
111 | ));
112 |
113 | export default Entry;
114 |
--------------------------------------------------------------------------------
/example/todo-web/src/containers/List.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: "off" */
2 | import React from 'react';
3 | import { Link } from 'react-router-dom';
4 | import {
5 | createSelector,
6 | } from 'reselect';
7 | import { connect } from 'react-redux';
8 | import {
9 | compose,
10 | withState,
11 | withProps,
12 | withHandlers,
13 | } from 'recompose';
14 | import {
15 | callMethod,
16 | } from 'ddp-redux/lib/actions';
17 | import Input from 'antd/lib/input';
18 | import Checkbox from 'antd/lib/checkbox';
19 | import Button from 'antd/lib/button';
20 | import ddp from 'ddp-connector';
21 | import {
22 | insert,
23 | update,
24 | // remove,
25 | todosInList,
26 | } from '../common/api/Todos';
27 | import {
28 | oneList,
29 | } from '../common/api/TodoLists';
30 | import Todo from '../common/models/Todo';
31 | import TodoList from '../common/models/TodoList';
32 | import Loader from '../components/Loader';
33 |
34 | const ListItem = withHandlers({
35 | onChange: ({
36 | todo,
37 | onUpdate,
38 | }) => event => onUpdate({
39 | todoId: todo._id,
40 | done: !!event.target.checked,
41 | name: todo.getName(),
42 | }),
43 | })(({
44 | todo,
45 | onChange,
46 | }) => (
47 |
50 |
57 | {todo.name}
58 |
59 |
60 | ));
61 |
62 | const List = compose(
63 | withState('name', 'setName', ''),
64 | withProps(({ match: { params: { listId } } }) => ({
65 | listId,
66 | })),
67 | ddp({
68 | subscriptions: (state, { listId }) => [
69 | oneList.withParams({ listId }),
70 | todosInList.withParams({ listId }),
71 | ],
72 | selectors: ({
73 | prop,
74 | from,
75 | }) => ({
76 | list: from(TodoList).select.one('listId'),
77 | todos: from(Todo).select.where(
78 | createSelector(
79 | prop('listId'),
80 | listId => todo => todo.getListId() === listId,
81 | ),
82 | ),
83 | }),
84 | loader: Loader,
85 | }),
86 | connect(
87 | null,
88 | (dispatch, {
89 | name,
90 | setName,
91 | listId,
92 | }) => ({
93 | onAddTodo: () =>
94 | dispatch(callMethod(insert.name, [{ listId, name }]))
95 | .then(() => setName('')),
96 |
97 | onUpdateTodo: ({ todoId, name, done }) =>
98 | dispatch(callMethod(update.name, [{ todoId, done, name }], {
99 | entities: {
100 | [Todo.collection]: {
101 | [todoId]: {
102 | done,
103 | name,
104 | },
105 | },
106 | },
107 | })),
108 | }),
109 | ),
110 | withHandlers({
111 | onChangeName: ({
112 | setName,
113 | }) => e => setName(e.currentTarget.value),
114 | }),
115 | )(({
116 | list,
117 | todos,
118 | name,
119 | onAddTodo,
120 | onChangeName,
121 | onUpdateTodo,
122 | }) => (
123 |
124 |
< Back
125 |
{list && list.getTitle()}
126 |
138 |
139 |
145 |
146 |
147 | ));
148 |
149 | export default List;
150 |
--------------------------------------------------------------------------------
/example/todo-web/src/containers/Lists.js:
--------------------------------------------------------------------------------
1 | /* eslint no-underscore-dangle: "off" */
2 | import React from 'react';
3 | import {
4 | compose,
5 | withState,
6 | withHandlers,
7 | lifecycle,
8 | } from 'recompose';
9 | import { Link } from 'react-router-dom';
10 | import {
11 | callMethod,
12 | } from 'ddp-redux/lib/actions';
13 | import ddp from 'ddp-connector';
14 | import {
15 | logout,
16 | queryRefetchAll,
17 | } from 'ddp-redux';
18 | import Input from 'antd/lib/input';
19 | import Button from 'antd/lib/button';
20 | import List from 'antd/lib/list';
21 | import {
22 | insert,
23 | allLists,
24 | } from '../common/api/TodoLists';
25 | import {
26 | getStats,
27 | } from '../common/api/Todos';
28 | import Loader from '../components/Loader';
29 | import TodoList from '../common/models/TodoList';
30 |
31 | const Lists = compose(
32 | lifecycle({
33 | componentDidMount() {
34 | const {
35 | dispatch,
36 | } = this.props;
37 | // NOTE: Of course this is suboptimal.
38 | // It will be improved after refactoring queries api.
39 | dispatch(queryRefetchAll());
40 | },
41 | }),
42 | withState('title', 'setTitle', ''),
43 | ddp({
44 | subscriptions: [
45 | allLists.withParams(),
46 | ],
47 | queries: [
48 | getStats.withParams(),
49 | ],
50 | selectors: ({
51 | from,
52 | }) => ({
53 | lists: from(TodoList).select.all(),
54 | }),
55 | loader: Loader,
56 | }),
57 | withHandlers({
58 | onLogout: ({ dispatch }) => () => dispatch(logout()),
59 | onAddList: ({
60 | title,
61 | setTitle,
62 | dispatch,
63 | }) => () =>
64 | dispatch(callMethod(insert.name, [{ title }]))
65 | .then(() => setTitle('')),
66 | onChangeTitle: ({
67 | setTitle,
68 | }) => e => setTitle(e.currentTarget.value),
69 | }),
70 | )(({
71 | lists,
72 | title,
73 | onLogout,
74 | onAddList,
75 | onChangeTitle,
76 | }) => (
77 |
78 |
Todo Lists
79 |
80 |
83 |
84 |
(
88 |
89 |
92 | {list.title}
93 |
94 | }
95 | description={`Active: ${list.active || 0}, completed: ${list.completed || 0}`}
96 | />
97 |
98 | )}
99 | />
100 |
101 |
106 |
107 |
108 |
109 |
110 |
111 | ));
112 |
113 | export default Lists;
114 |
--------------------------------------------------------------------------------
/example/todo-web/src/containers/LoggedInRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Redirect, Route } from 'react-router-dom';
4 | import ddp from 'ddp-connector';
5 |
6 | const createLoggedInRoute = ({
7 | loader,
8 | redirectTo,
9 | }) => {
10 | const DecoratedComponent = ddp({
11 | loader,
12 | selectors: ({ from, select }) => ({
13 | currentUser: from('users').select.currentUser(),
14 | isLoggingIn: select.isLoggingIn(),
15 | }),
16 | })(({
17 | currentUser,
18 | isLoggingIn,
19 | component,
20 | ...props
21 | }) => {
22 | if (isLoggingIn) {
23 | return null;
24 | }
25 | if (currentUser) {
26 | return React.createElement(component, {
27 | ...props,
28 | currentUser,
29 | });
30 | }
31 | if (redirectTo) {
32 | return React.createElement(Redirect, {
33 | to: {
34 | pathname: redirectTo,
35 | state: { from: props.location },
36 | hash: props.location.hash,
37 | },
38 | });
39 | }
40 | return null;
41 | });
42 |
43 | const Container = ({ component, ...rest }) => React.createElement(Route, {
44 | ...rest,
45 | render: props => React.createElement(DecoratedComponent, {
46 | ...props,
47 | component,
48 | }),
49 | });
50 |
51 | Container.propTypes = {
52 | component: PropTypes.func.isRequired,
53 | };
54 |
55 | return Container;
56 | };
57 |
58 | const LoggedInRoute = createLoggedInRoute({
59 | redirectTo: '/entry',
60 | });
61 |
62 | export { createLoggedInRoute };
63 | export default LoggedInRoute;
64 |
--------------------------------------------------------------------------------
/example/todo-web/src/index.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 |
3 | body {
4 | margin: 0;
5 | padding: 0;
6 | font-family: sans-serif;
7 | }
8 |
9 | .container {
10 | width: 50%;
11 | max-width: 400px;
12 | margin: auto;
13 | margin-top: 10%;
14 | }
15 |
--------------------------------------------------------------------------------
/example/todo-web/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: "off" */
2 | import {
3 | persistState,
4 | } from 'redux-devtools';
5 | import {
6 | compose,
7 | createStore,
8 | applyMiddleware,
9 | } from 'redux';
10 | import DDPClient from 'ddp-redux';
11 | import React from 'react';
12 | import ReactDOM from 'react-dom';
13 | import notification from 'antd/lib/notification';
14 | import App from './containers/App';
15 | import registerServiceWorker from './registerServiceWorker';
16 | import rootReducer from './store/rootReducer';
17 | import storage from './storage';
18 | import './index.css';
19 |
20 | const ddpClient = new DDPClient({
21 | endpoint: process.env.REACT_APP_ENDPOINT,
22 | SocketConstructor: WebSocket,
23 | storage,
24 | });
25 |
26 | ddpClient.onPromise = ((promise) => {
27 | promise.catch((err) => {
28 | notification.error({
29 | message: 'Error',
30 | description: err.reason || err.message,
31 | });
32 | });
33 | });
34 |
35 | const enhancer = compose(
36 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__({}) : x => x,
37 | persistState(
38 | window.location.href.match(
39 | /[?&]debug_session=([^]+)\b/,
40 | ),
41 | ),
42 | );
43 |
44 | const store = createStore(
45 | rootReducer,
46 | {},
47 | compose(
48 | applyMiddleware(
49 | ddpClient.middleware(),
50 | ),
51 | enhancer,
52 | ),
53 | );
54 |
55 | ReactDOM.render(
56 | ,
57 | document.getElementById('root'),
58 | );
59 |
60 | if (process.env.NODE_ENV !== 'production') {
61 | if (typeof module !== 'undefined' && module.hot) {
62 | module.hot.accept('./containers/App', () => {
63 | const NextApp = require('./containers/App').default;
64 | ReactDOM.render(
65 | ,
66 | document.getElementById('root'),
67 | );
68 | });
69 |
70 | module.hot.accept('./store/rootReducer.js', () =>
71 | store.replaceReducer(require('./store/rootReducer.js').default),
72 | );
73 | }
74 | }
75 |
76 | registerServiceWorker();
77 |
--------------------------------------------------------------------------------
/example/todo-web/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/example/todo-web/src/routes/Router.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Redirect,
4 | BrowserRouter,
5 | Switch,
6 | Route,
7 | } from 'react-router-dom';
8 | import List from '../containers/List';
9 | import Lists from '../containers/Lists';
10 | import Entry from '../containers/Entry';
11 | import NotFound from '../components/NotFound';
12 | import LoggedInRoute from '../containers/LoggedInRoute';
13 |
14 | const Router = () => (
15 |
16 |
17 | (
21 |
26 | )}
27 | />
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 |
36 | export default Router;
37 |
--------------------------------------------------------------------------------
/example/todo-web/src/storage.js:
--------------------------------------------------------------------------------
1 | const storage = {
2 | set: (key, value) => localStorage.setItem(key, value),
3 | del: key => localStorage.removeItem(key),
4 | get: key => localStorage.getItem(key),
5 | };
6 |
7 | export default storage;
8 |
--------------------------------------------------------------------------------
/example/todo-web/src/store/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import DDPClient from 'ddp-redux';
3 |
4 | const rootReducer = combineReducers({
5 | ddp: DDPClient.reducer(),
6 | });
7 |
8 | export default rootReducer;
9 |
--------------------------------------------------------------------------------