diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6727bcd97d..ce118f8ebd 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -72,7 +72,7 @@ jobs:
lint-unit-27:
<<: *lint-unit
docker:
- - image: circleci/python:2.7-stretch-node-browsers
+ - image: circleci/python:2.7.18-stretch-node-browsers
environment:
PYLINTRC: .pylintrc
PYVERSION: python27
@@ -122,7 +122,7 @@ jobs:
build-core-27:
<<: *build-core
docker:
- - image: circleci/python:2.7-stretch-node-browsers
+ - image: circleci/python:2.7.18-stretch-node-browsers
environment:
PYVERSION: python27
@@ -172,7 +172,7 @@ jobs:
build-misc-27:
<<: *build-misc
docker:
- - image: circleci/python:2.7-stretch-node-browsers
+ - image: circleci/python:2.7.18-stretch-node-browsers
environment:
PYVERSION: python27
@@ -372,7 +372,7 @@ jobs:
test-27:
<<: *test
docker:
- - image: circleci/python:2.7-stretch-node-browsers
+ - image: circleci/python:2.7.18-stretch-node-browsers
environment:
PERCY_ENABLE: 0
PYVERSION: python27
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe567b9c32..4312e8ca04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- [#1237](https://github.com/plotly/dash/pull/1237) Closes [#920](https://github.com/plotly/dash/issues/920): Converts hot reload fetch failures into a server status indicator showing whether the latest fetch succeeded or failed. Callback fetch failures still appear as errors but have a clearer message.
+- [#1254](https://github.com/plotly/dash/pull/1254) Modifies the callback chain implementation and improves performance for apps with a lot of components
### Fixed
- [#1255](https://github.com/plotly/dash/pull/1255) Hard hot reload targets only the current window, not the top - so if your app is in an iframe you will only reload the app
diff --git a/dash-renderer/@Types/modules.d.ts b/dash-renderer/@Types/modules.d.ts
new file mode 100644
index 0000000000..042d739872
--- /dev/null
+++ b/dash-renderer/@Types/modules.d.ts
@@ -0,0 +1,9 @@
+declare module 'cookie' {
+ const value: {
+ parse: (cookie: string) => {
+ _csrf_token: string
+ }
+ };
+
+ export default value;
+}
diff --git a/dash-renderer/babel.config.js b/dash-renderer/babel.config.js
index aee2bac0c7..455e1966b4 100644
--- a/dash-renderer/babel.config.js
+++ b/dash-renderer/babel.config.js
@@ -1,11 +1,16 @@
module.exports = {
presets: [
+ '@babel/preset-typescript',
'@babel/preset-env',
'@babel/preset-react'
],
+ plugins: [
+ '@babel/plugin-proposal-class-properties',
+ ],
env: {
test: {
plugins: [
+ '@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-modules-commonjs'
]
}
diff --git a/dash-renderer/jest.config.js b/dash-renderer/jest.config.js
index bfe097b036..4e05b43997 100644
--- a/dash-renderer/jest.config.js
+++ b/dash-renderer/jest.config.js
@@ -85,7 +85,7 @@ module.exports = {
// notifyMode: "always",
// A preset that is used as a base for Jest's configuration
- // preset: null,
+ preset: "ts-jest/presets/js-with-babel",
// Run tests from one or more projects
// projects: null,
diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json
index 1c452c4a1d..58209deaf0 100644
--- a/dash-renderer/package-lock.json
+++ b/dash-renderer/package-lock.json
@@ -398,6 +398,164 @@
"semver": "^5.5.0"
}
},
+ "@babel/helper-create-class-features-plugin": {
+ "version": "7.10.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.2.tgz",
+ "integrity": "sha512-5C/QhkGFh1vqcziq1vAL6SI9ymzUp8BCYjFpvYVhWP4DlATIb3u5q3iUd35mvlyGs8fO7hckkW7i0tmH+5+bvQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-function-name": "^7.10.1",
+ "@babel/helper-member-expression-to-functions": "^7.10.1",
+ "@babel/helper-optimise-call-expression": "^7.10.1",
+ "@babel/helper-plugin-utils": "^7.10.1",
+ "@babel/helper-replace-supers": "^7.10.1",
+ "@babel/helper-split-export-declaration": "^7.10.1"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
+ "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.1"
+ }
+ },
+ "@babel/generator": {
+ "version": "7.10.2",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.2.tgz",
+ "integrity": "sha512-AxfBNHNu99DTMvlUPlt1h2+Hn7knPpH5ayJ8OqDWSeLld+Fi2AYBTC/IejWDM9Edcii4UzZRCsbUt0WlSDsDsA==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.2",
+ "jsesc": "^2.5.1",
+ "lodash": "^4.17.13",
+ "source-map": "^0.5.0"
+ }
+ },
+ "@babel/helper-function-name": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.1.tgz",
+ "integrity": "sha512-fcpumwhs3YyZ/ttd5Rz0xn0TpIwVkN7X0V38B9TWNfVF42KEkhkAAuPCQ3oXmtTRtiPJrmZ0TrfS0GKF0eMaRQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-get-function-arity": "^7.10.1",
+ "@babel/template": "^7.10.1",
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/helper-get-function-arity": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.1.tgz",
+ "integrity": "sha512-F5qdXkYGOQUb0hpRaPoetF9AnsXknKjWMZ+wmsIRsp5ge5sFh4c3h1eH2pRTTuy9KKAA2+TTYomGXAtEL2fQEw==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/helper-member-expression-to-functions": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.1.tgz",
+ "integrity": "sha512-u7XLXeM2n50gb6PWJ9hoO5oO7JFPaZtrh35t8RqKLT1jFKj9IWeD1zrcrYp1q1qiZTdEarfDWfTIP8nGsu0h5g==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/helper-optimise-call-expression": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.1.tgz",
+ "integrity": "sha512-a0DjNS1prnBsoKx83dP2falChcs7p3i8VMzdrSbfLhuQra/2ENC4sbri34dz/rWmDADsmF1q5GbfaXydh0Jbjg==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz",
+ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==",
+ "dev": true
+ },
+ "@babel/helper-replace-supers": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz",
+ "integrity": "sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-member-expression-to-functions": "^7.10.1",
+ "@babel/helper-optimise-call-expression": "^7.10.1",
+ "@babel/traverse": "^7.10.1",
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/helper-split-export-declaration": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz",
+ "integrity": "sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz",
+ "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.1",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@babel/parser": {
+ "version": "7.10.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.2.tgz",
+ "integrity": "sha512-PApSXlNMJyB4JiGVhCOlzKIif+TKFTvu0aQAhnTvfP/z3vVSN6ZypH5bfUNwFXXjRQtUEBNFd2PtmCmG2Py3qQ==",
+ "dev": true
+ },
+ "@babel/template": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.1.tgz",
+ "integrity": "sha512-OQDg6SqvFSsc9A0ej6SKINWrpJiNonRIniYondK2ViKhB06i3c0s+76XUft71iqBEe9S1OKsHwPAjfHnuvnCig==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.1",
+ "@babel/parser": "^7.10.1",
+ "@babel/types": "^7.10.1"
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.1.tgz",
+ "integrity": "sha512-C/cTuXeKt85K+p08jN6vMDz8vSV0vZcI0wmQ36o6mjbuo++kPMdpOYw23W2XH04dbRt9/nMEfA4W3eR21CD+TQ==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.10.1",
+ "@babel/generator": "^7.10.1",
+ "@babel/helper-function-name": "^7.10.1",
+ "@babel/helper-split-export-declaration": "^7.10.1",
+ "@babel/parser": "^7.10.1",
+ "@babel/types": "^7.10.1",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0",
+ "lodash": "^4.17.13"
+ }
+ },
+ "@babel/types": {
+ "version": "7.10.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.2.tgz",
+ "integrity": "sha512-AD3AwWBSz0AWF0AkCN9VPiWrvldXq+/e3cHa4J89vo4ymjz1XwrBFFVZmkJTsQIPNk+ZVomPSXUJqq8yyjZsng==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.10.1",
+ "lodash": "^4.17.13",
+ "to-fast-properties": "^2.0.0"
+ }
+ }
+ }
+ },
"@babel/helper-create-regexp-features-plugin": {
"version": "7.8.6",
"resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.6.tgz",
@@ -1115,6 +1273,12 @@
"@babel/types": "^7.7.0"
}
},
+ "@babel/helper-validator-identifier": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz",
+ "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==",
+ "dev": true
+ },
"@babel/helper-wrap-function": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz",
@@ -1382,6 +1546,24 @@
"@babel/plugin-syntax-async-generators": "^7.8.0"
}
},
+ "@babel/plugin-proposal-class-properties": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz",
+ "integrity": "sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-class-features-plugin": "^7.10.1",
+ "@babel/helper-plugin-utils": "^7.10.1"
+ },
+ "dependencies": {
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz",
+ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==",
+ "dev": true
+ }
+ }
+ },
"@babel/plugin-proposal-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz",
@@ -1550,6 +1732,23 @@
"@babel/helper-plugin-utils": "^7.8.3"
}
},
+ "@babel/plugin-syntax-typescript": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.1.tgz",
+ "integrity": "sha512-X/d8glkrAtra7CaQGMiGs/OGa6XgUzqPcBXCIGFCpCqnfGlT0Wfbzo/B89xHhnInTaItPK8LALblVXcUOEh95Q==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.1"
+ },
+ "dependencies": {
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz",
+ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==",
+ "dev": true
+ }
+ }
+ },
"@babel/plugin-transform-arrow-functions": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz",
@@ -2108,6 +2307,25 @@
"@babel/helper-plugin-utils": "^7.8.3"
}
},
+ "@babel/plugin-transform-typescript": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.1.tgz",
+ "integrity": "sha512-v+QWKlmCnsaimLeqq9vyCsVRMViZG1k2SZTlcZvB+TqyH570Zsij8nvVUZzOASCRiQFUxkLrn9Wg/kH0zgy5OQ==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-create-class-features-plugin": "^7.10.1",
+ "@babel/helper-plugin-utils": "^7.10.1",
+ "@babel/plugin-syntax-typescript": "^7.10.1"
+ },
+ "dependencies": {
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz",
+ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==",
+ "dev": true
+ }
+ }
+ },
"@babel/plugin-transform-unicode-regex": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz",
@@ -2238,6 +2456,24 @@
}
}
},
+ "@babel/preset-typescript": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.10.1.tgz",
+ "integrity": "sha512-m6GV3y1ShiqxnyQj10600ZVOFrSSAa8HQ3qIUk2r+gcGtHTIRw0dJnFLt1WNXpKjtVw7yw1DAPU/6ma2ZvgJuA==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.1",
+ "@babel/plugin-transform-typescript": "^7.10.1"
+ },
+ "dependencies": {
+ "@babel/helper-plugin-utils": {
+ "version": "7.10.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.1.tgz",
+ "integrity": "sha512-fvoGeXt0bJc7VMWZGCAEBEMo/HAjW2mP8apF5eXK0wSqwLAVHAISCWRoLMBMUs2kqeaG77jltVqu4Hn8Egl3nA==",
+ "dev": true
+ }
+ }
+ },
"@babel/runtime": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.2.tgz",
@@ -3246,6 +3482,16 @@
"@types/node": "*"
}
},
+ "@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
@@ -3301,12 +3547,64 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
+ "@types/prop-types": {
+ "version": "15.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+ "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
+ "dev": true
+ },
"@types/q": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
"dev": true
},
+ "@types/ramda": {
+ "version": "0.27.6",
+ "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.27.6.tgz",
+ "integrity": "sha512-ephagb0ZIAJSoS5I/qMS4Mqo1b/Nd50pWM+o1QO/dz8NF//GsCGPTLDVRqgXlVncy74KShfHzE5rPZXTeek4PA==",
+ "dev": true,
+ "requires": {
+ "ts-toolbelt": "^6.3.3"
+ }
+ },
+ "@types/react": {
+ "version": "16.9.34",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.34.tgz",
+ "integrity": "sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^2.2.0"
+ }
+ },
+ "@types/react-redux": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.7.tgz",
+ "integrity": "sha512-U+WrzeFfI83+evZE2dkZ/oF/1vjIYgqrb5dGgedkqVV8HEfDFujNgWCwHL89TDuWKb47U0nTBT6PLGq4IIogWg==",
+ "dev": true,
+ "requires": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
+ "@types/redux": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@types/redux/-/redux-3.6.0.tgz",
+ "integrity": "sha1-8evh5UEVGAcuT9/KXHbhbnTBOZo=",
+ "dev": true,
+ "requires": {
+ "redux": "*"
+ }
+ },
+ "@types/redux-actions": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/@types/redux-actions/-/redux-actions-2.6.1.tgz",
+ "integrity": "sha512-zKgK+ATp3sswXs6sOYo1tk8xdXTy4CTaeeYrVQlClCjeOpag5vzPo0ASWiiBJ7vsiQRAdb3VkuFLnDoBimF67g==",
+ "dev": true
+ },
"@types/stack-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
@@ -4594,6 +4892,15 @@
}
}
},
+ "bs-logger": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+ "dev": true,
+ "requires": {
+ "fast-json-stable-stringify": "2.x"
+ }
+ },
"bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -4632,6 +4939,12 @@
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true
},
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+ "dev": true
+ },
"builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
@@ -5599,6 +5912,12 @@
}
}
},
+ "csstype": {
+ "version": "2.6.10",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz",
+ "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==",
+ "dev": true
+ },
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@@ -5849,6 +6168,12 @@
"integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
"dev": true
},
+ "diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true
+ },
"diff-sequences": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.1.0.tgz",
@@ -12058,6 +12383,12 @@
"semver": "^5.6.0"
}
},
+ "make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
"make-plural": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
@@ -18315,18 +18646,220 @@
"integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==",
"dev": true
},
+ "ts-jest": {
+ "version": "26.0.0",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.0.0.tgz",
+ "integrity": "sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw==",
+ "dev": true,
+ "requires": {
+ "bs-logger": "0.x",
+ "buffer-from": "1.x",
+ "fast-json-stable-stringify": "2.x",
+ "json5": "2.x",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "micromatch": "4.x",
+ "mkdirp": "1.x",
+ "semver": "7.x",
+ "yargs-parser": "18.x"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
+ "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ }
+ }
+ },
+ "ts-loader": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.2.tgz",
+ "integrity": "sha512-DwpZFB67RoILQHx42dMjSgv2STpacsQu5X+GD/H9ocd8IhU0m8p3b/ZrIln2KmcucC6xep2PdEMEblpWT71euA==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.3.0",
+ "enhanced-resolve": "^4.0.0",
+ "loader-utils": "^1.0.2",
+ "micromatch": "^4.0.0",
+ "semver": "^6.0.0"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ }
+ }
+ },
+ "ts-toolbelt": {
+ "version": "6.9.4",
+ "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.9.4.tgz",
+ "integrity": "sha512-muRZZqfOTOVvLk5cdnp7YWm6xX+kD/WL2cS/L4zximBRcbQSuMoTbQQ2ZZBVMs1gB0EZw1qThP+HrIQB35OmEw==",
+ "dev": true
+ },
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
"dev": true
},
+ "tslint": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.2.tgz",
+ "integrity": "sha512-UyNrLdK3E0fQG/xWNqAFAC5ugtFyPO4JJR1KyyfQAyzR8W0fTRrC91A8Wej4BntFzcvETdCSDa/4PnNYJQLYiA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.3",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.10.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ }
+ }
+ },
"tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"dev": true
},
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
diff --git a/dash-renderer/package.json b/dash-renderer/package.json
index 3b383ef7f6..a2c1b48606 100644
--- a/dash-renderer/package.json
+++ b/dash-renderer/package.json
@@ -4,11 +4,13 @@
"description": "render dash components in react",
"main": "dash_renderer/dash_renderer.min.js",
"scripts": {
- "prepublishOnly": "rm -rf lib && babel src --out-dir lib --copy-files",
+ "prepublishOnly": "rm -rf lib && babel src --extensions=\".ts,.tsx,.js,.jsx\" --out-dir lib --copy-files",
"private::format.js-eslint": "eslint --quiet --fix .",
"private::format.js-prettier": "prettier --config .prettierrc --write \"src/**/*.js\"",
+ "private::format.ts": "tslint --fix --project tsconfig.json --config tslint.json",
"private::lint.js-eslint": "eslint .",
"private::lint.js-prettier": "prettier --config .prettierrc \"src/**/*.js\" --list-different",
+ "private::lint.ts": "tslint --project tsconfig.json --config tslint.json",
"build:js": "webpack --build release",
"build:dev": "webpack --build local",
"build:local": "renderer build local",
@@ -41,10 +43,17 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.8.7",
+ "@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
"@babel/preset-env": "^7.8.7",
"@babel/preset-react": "^7.8.3",
+ "@babel/preset-typescript": "^7.10.1",
"@svgr/webpack": "^5.2.0",
+ "@types/ramda": "^0.27.6",
+ "@types/react": "^16.9.34",
+ "@types/react-redux": "^7.1.7",
+ "@types/redux": "^3.6.0",
+ "@types/redux-actions": "^2.6.1",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.4.2",
@@ -63,6 +72,10 @@
"prettier-eslint-cli": "^5.0.0",
"prettier-stylelint": "^0.4.2",
"style-loader": "^1.1.3",
+ "ts-jest": "^26.0.0",
+ "ts-loader": "^7.0.2",
+ "tslint": "^6.1.2",
+ "typescript": "^3.8.3",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js
index 23e5f34c01..8479b0fd07 100644
--- a/dash-renderer/src/APIController.react.js
+++ b/dash-renderer/src/APIController.react.js
@@ -1,6 +1,6 @@
import {connect} from 'react-redux';
import {includes, isEmpty} from 'ramda';
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState, createContext} from 'react';
import PropTypes from 'prop-types';
import TreeContainer from './TreeContainer';
import GlobalErrorContainer from './components/error/GlobalErrorContainer.react';
@@ -19,6 +19,9 @@ import {EventEmitter} from './actions/utils';
import {applyPersistence} from './persistence';
import {getAppState} from './reducers/constants';
import {STATUS} from './constants/constants';
+import {getLoadingState, getLoadingHash} from './utils/TreeContainer';
+
+export const DashContext = createContext({});
/**
* Fire off API calls for initialization
@@ -26,6 +29,16 @@ import {STATUS} from './constants/constants';
* @returns {*} component
*/
const UnconnectedContainer = props => {
+ const {
+ appLifecycle,
+ config,
+ dependenciesRequest,
+ error,
+ layoutRequest,
+ layout,
+ loadingMap,
+ } = props;
+
const [errorLoading, setErrorLoading] = useState(false);
const events = useRef(null);
@@ -34,6 +47,18 @@ const UnconnectedContainer = props => {
}
const renderedTree = useRef(false);
+ const propsRef = useRef({});
+ propsRef.current = props;
+
+ const provider = useRef({
+ fn: () => ({
+ _dashprivate_config: propsRef.current.config,
+ _dashprivate_dispatch: propsRef.current.dispatch,
+ _dashprivate_graphs: propsRef.current.graphs,
+ _dashprivate_loadingMap: propsRef.current.loadingMap,
+ }),
+ });
+
useEffect(storeEffect.bind(null, props, events, setErrorLoading));
useEffect(() => {
@@ -43,14 +68,6 @@ const UnconnectedContainer = props => {
}
});
- const {
- appLifecycle,
- dependenciesRequest,
- layoutRequest,
- layout,
- config,
- } = props;
-
let content;
if (
layoutRequest.status &&
@@ -65,11 +82,24 @@ const UnconnectedContainer = props => {
content =
Error loading dependencies
;
} else if (appLifecycle === getAppState('HYDRATED')) {
renderedTree.current = true;
+
content = (
-
+
+
+
);
} else {
content = Loading...
;
@@ -157,6 +187,7 @@ UnconnectedContainer.propTypes = {
graphs: PropTypes.object,
layoutRequest: PropTypes.object,
layout: PropTypes.object,
+ loadingMap: PropTypes.any,
history: PropTypes.any,
error: PropTypes.object,
config: PropTypes.object,
@@ -169,6 +200,7 @@ const Container = connect(
dependenciesRequest: state.dependenciesRequest,
layoutRequest: state.layoutRequest,
layout: state.layout,
+ loadingMap: state.loadingMap,
graphs: state.graphs,
history: state.history,
error: state.error,
diff --git a/dash-renderer/src/AppProvider.react.js b/dash-renderer/src/AppProvider.react.tsx
similarity index 82%
rename from dash-renderer/src/AppProvider.react.js
rename to dash-renderer/src/AppProvider.react.tsx
index d44a27eb61..6b534be900 100644
--- a/dash-renderer/src/AppProvider.react.js
+++ b/dash-renderer/src/AppProvider.react.tsx
@@ -1,14 +1,13 @@
+import PropTypes from 'prop-types';
import React from 'react';
import {Provider} from 'react-redux';
import initializeStore from './store';
import AppContainer from './AppContainer.react';
-import PropTypes from 'prop-types';
-
const store = initializeStore();
-const AppProvider = ({hooks}) => {
+const AppProvider = ({hooks}: any) => {
return (
@@ -19,15 +18,17 @@ const AppProvider = ({hooks}) => {
AppProvider.propTypes = {
hooks: PropTypes.shape({
request_pre: PropTypes.func,
- request_post: PropTypes.func,
- }),
+ request_post: PropTypes.func
+ })
};
AppProvider.defaultProps = {
hooks: {
request_pre: null,
- request_post: null,
- },
+ request_post: null
+ }
};
export default AppProvider;
+
+
diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts
new file mode 100644
index 0000000000..4bc82382f0
--- /dev/null
+++ b/dash-renderer/src/StoreObserver.ts
@@ -0,0 +1,113 @@
+import {
+ any,
+ filter,
+ forEach,
+ map,
+ path
+} from 'ramda';
+
+import { Store, Unsubscribe } from 'redux';
+
+type Observer = (store: TStore) => void;
+type UnregisterObserver = () => void;
+
+interface IStoreObserverState {
+ inputPaths: string[][];
+ lastState: any;
+ observer: Observer;
+ triggered: boolean;
+}
+
+export interface IStoreObserverDefinition {
+ observer: Observer>;
+ inputs: string[]
+}
+
+export default class StoreObserver {
+ private _store?: Store;
+ private _unsubscribe?: Unsubscribe;
+
+ private readonly _observers: IStoreObserverState>[] = [];
+
+ constructor(store?: Store) {
+ this.__init__(store);
+ }
+
+ observe = (
+ observer: IStoreObserverDefinition | Observer>,
+ inputs?: string[]
+ ): UnregisterObserver => {
+ if (typeof observer === 'function') {
+ if (!Array.isArray(inputs)) {
+ throw new Error('inputs must be an array');
+ }
+
+ this.add(observer, inputs);
+ return () => this.remove(observer);
+ } else {
+ this.add(observer.observer, observer.inputs);
+ return () => this.remove(observer.observer);
+ }
+ }
+
+ setStore = (store: Store) => {
+ this.__finalize__();
+ this.__init__(store);
+ }
+
+ private __finalize__ = () => this._unsubscribe?.()
+
+ private __init__ = (store?: Store) => {
+ this._store = store;
+ if (store) {
+ this._unsubscribe = store.subscribe(this.notify);
+ }
+
+ forEach(o => o.lastState = null, this._observers);
+ }
+
+ private add = (
+ observer: Observer>,
+ inputs: string[]
+ ) => this._observers.push({
+ inputPaths: map(p => p.split('.'), inputs),
+ lastState: null,
+ observer,
+ triggered: false
+ });
+
+ private notify = () => {
+ const store = this._store;
+ if (!store) {
+ return;
+ }
+
+ const state = store.getState();
+
+ const triggered = filter(
+ o => !o.triggered && any(
+ i => path(i, state) !== path(i, o.lastState),
+ o.inputPaths
+ ),
+ this._observers
+ );
+
+ forEach(o => o.triggered = true, triggered);
+
+ forEach(
+ o => {
+ o.lastState = store.getState();
+ o.observer(store);
+ o.triggered = false;
+ },
+ triggered
+ );
+ }
+
+ private remove = (observer: Observer>) => this._observers.splice(
+ this._observers.findIndex(
+ o => observer === o.observer,
+ this._observers
+ ), 1
+ );
+}
diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js
index 3b1c3c13c2..bf317ad17d 100644
--- a/dash-renderer/src/TreeContainer.js
+++ b/dash-renderer/src/TreeContainer.js
@@ -1,15 +1,12 @@
-import React, {Component} from 'react';
+import React, {Component, memo} from 'react';
import PropTypes from 'prop-types';
import Registry from './registry';
import {propTypeErrorHandler} from './exceptions';
-import {connect} from 'react-redux';
import {
addIndex,
concat,
dissoc,
equals,
- filter,
- has,
isEmpty,
isNil,
keys,
@@ -26,46 +23,16 @@ import {recordUiEdit} from './persistence';
import ComponentErrorBoundary from './components/error/ComponentErrorBoundary.react';
import checkPropTypes from './checkPropTypes';
import {getWatchedKeys, stringifyId} from './actions/dependencies';
-
-function validateComponent(componentDefinition) {
- if (type(componentDefinition) === 'Array') {
- throw new Error(
- 'The children property of a component is a list of lists, instead ' +
- 'of just a list. ' +
- 'Check the component that has the following contents, ' +
- 'and remove one of the levels of nesting: \n' +
- JSON.stringify(componentDefinition, null, 2)
- );
- }
- if (
- type(componentDefinition) === 'Object' &&
- !(
- has('namespace', componentDefinition) &&
- has('type', componentDefinition) &&
- has('props', componentDefinition)
- )
- ) {
- throw new Error(
- 'An object was provided as `children` instead of a component, ' +
- 'string, or number (or list of those). ' +
- 'Check the children property that looks something like:\n' +
- JSON.stringify(componentDefinition, null, 2)
- );
- }
-}
-
-const createContainer = (component, path) =>
- isSimpleComponent(component) ? (
- component
- ) : (
-
- );
+import {
+ getLoadingHash,
+ getLoadingState,
+ validateComponent,
+} from './utils/TreeContainer';
+import {DashContext} from './APIController.react';
+
+const NOT_LOADING = {
+ is_loading: false,
+};
function CheckedComponent(p) {
const {element, extraProps, props, children, type} = p;
@@ -100,13 +67,51 @@ function createElement(element, props, extraProps, children) {
return React.createElement(element, allProps, children);
}
-class TreeContainer extends Component {
+const TreeContainer = memo(props => (
+
+ {context => (
+
+ )}
+
+));
+
+class BaseTreeContainer extends Component {
constructor(props) {
super(props);
this.setProps = this.setProps.bind(this);
}
+ createContainer(props, component, path) {
+ return isSimpleComponent(component) ? (
+ component
+ ) : (
+
+ );
+ }
+
setProps(newProps) {
const {
_dashprivate_graphs,
@@ -161,17 +166,26 @@ class TreeContainer extends Component {
return Array.isArray(components)
? addIndex(map)(
(component, i) =>
- createContainer(
+ this.createContainer(
+ this.props,
component,
concat(path, ['props', 'children', i])
),
components
)
- : createContainer(components, concat(path, ['props', 'children']));
+ : this.createContainer(
+ this.props,
+ components,
+ concat(path, ['props', 'children'])
+ );
}
getComponent(_dashprivate_layout, children, loading_state, setProps) {
- const {_dashprivate_config} = this.props;
+ const {
+ _dashprivate_config,
+ _dashprivate_dispatch,
+ _dashprivate_error,
+ } = this.props;
if (isEmpty(_dashprivate_layout)) {
return null;
@@ -192,13 +206,18 @@ class TreeContainer extends Component {
// just the id we pass on to the rendered component
props.id = stringifyId(props.id);
}
- const extraProps = {loading_state, setProps};
+ const extraProps = {
+ loading_state: loading_state || NOT_LOADING,
+ setProps,
+ };
return (
{_dashprivate_config.props_check ? (
- !isSimpleComponent(child) && !isLoadingComponent(child),
- Array.isArray(children) ? children : [children]
- );
-
- queue.push(...filteredChildren);
- }
- }
-
- return ids;
-}
-
-function getLoadingState(layout, pendingCallbacks) {
- const ids = isLoadingComponent(layout)
- ? getNestedIds(layout)
- : layout && layout.props.id && [layout.props.id];
-
- let isLoading = false;
- let loadingProp;
- let loadingComponent;
-
- if (pendingCallbacks && pendingCallbacks.length && ids && ids.length) {
- const idStrs = ids.map(stringifyId);
-
- pendingCallbacks.forEach(cb => {
- const {requestId, requestedOutputs} = cb;
- if (requestId === undefined) {
- return;
- }
-
- idStrs.forEach(idStr => {
- const props = requestedOutputs[idStr];
- if (props) {
- isLoading = true;
- // TODO: what about multiple loading components / props?
- loadingComponent = idStr;
- loadingProp = props[0];
- }
- });
- });
- }
-
- // Set loading state
- return {
- is_loading: isLoading,
- prop_name: loadingProp,
- component_name: loadingComponent,
- };
-}
-
-export const AugmentedTreeContainer = connect(
- state => ({
- graphs: state.graphs,
- pendingCallbacks: state.pendingCallbacks,
- config: state.config,
- }),
- dispatch => ({dispatch}),
- (stateProps, dispatchProps, ownProps) => ({
- _dashprivate_graphs: stateProps.graphs,
- _dashprivate_dispatch: dispatchProps.dispatch,
- _dashprivate_layout: ownProps._dashprivate_layout,
- _dashprivate_path: ownProps._dashprivate_path,
- _dashprivate_loadingState: getLoadingState(
- ownProps._dashprivate_layout,
- stateProps.pendingCallbacks
- ),
- _dashprivate_config: stateProps.config,
- })
-)(TreeContainer);
-
-export default AugmentedTreeContainer;
+export default TreeContainer;
diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts
new file mode 100644
index 0000000000..c660bcb218
--- /dev/null
+++ b/dash-renderer/src/actions/callbacks.ts
@@ -0,0 +1,422 @@
+import {
+ concat,
+ flatten,
+ keys,
+ map,
+ mergeDeepRight,
+ path,
+ pick,
+ pluck,
+ zip
+} from 'ramda';
+
+import { STATUS } from '../constants/constants';
+import { CallbackActionType, CallbackAggregateActionType } from '../reducers/callbacks';
+import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, ICallbackPayload, IStoredCallback, IBlockedCallback, IPrioritizedCallback } from '../types/callbacks';
+import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies';
+import { urlBase } from './utils';
+import { getCSRFHeader } from '.';
+import { createAction, Action } from 'redux-actions';
+
+export const addBlockedCallbacks = createAction(
+ CallbackActionType.AddBlocked
+);
+export const addCompletedCallbacks = createAction(
+ CallbackAggregateActionType.AddCompleted
+);
+export const addExecutedCallbacks = createAction(
+ CallbackActionType.AddExecuted
+);
+export const addExecutingCallbacks = createAction(
+ CallbackActionType.AddExecuting
+);
+export const addPrioritizedCallbacks = createAction(
+ CallbackActionType.AddPrioritized
+);
+export const addRequestedCallbacks = createAction(
+ CallbackActionType.AddRequested
+);
+export const addStoredCallbacks = createAction(
+ CallbackActionType.AddStored
+);
+export const addWatchedCallbacks = createAction(CallbackActionType.AddWatched);
+export const removeExecutedCallbacks = createAction(
+ CallbackActionType.RemoveExecuted
+);
+export const removeBlockedCallbacks = createAction(
+ CallbackActionType.RemoveBlocked
+);
+export const removeExecutingCallbacks = createAction(
+ CallbackActionType.RemoveExecuting
+);
+export const removePrioritizedCallbacks = createAction(
+ CallbackActionType.RemovePrioritized
+);
+export const removeRequestedCallbacks = createAction(
+ CallbackActionType.RemoveRequested
+);
+export const removeStoredCallbacks = createAction(
+ CallbackActionType.RemoveStored
+);
+export const removeWatchedCallbacks = createAction(
+ CallbackActionType.RemoveWatched
+);
+export const aggregateCallbacks = createAction<(
+ Action |
+ Action |
+ null
+)[]>(CallbackAggregateActionType.Aggregate);
+
+function unwrapIfNotMulti(
+ paths: any,
+ idProps: any,
+ spec: any,
+ anyVals: any,
+ depType: any
+) {
+ let msg = '';
+
+ if (isMultiValued(spec)) {
+ return [idProps, msg];
+ }
+
+ if (idProps.length !== 1) {
+ if (!idProps.length) {
+ const isStr = typeof spec.id === 'string';
+ msg =
+ 'A nonexistent object was used in an `' +
+ depType +
+ '` of a Dash callback. The id of this object is ' +
+ (isStr
+ ? '`' + spec.id + '`'
+ : JSON.stringify(spec.id) +
+ (anyVals ? ' with MATCH values ' + anyVals : '')) +
+ ' and the property is `' +
+ spec.property +
+ (isStr
+ ? '`. The string ids in the current layout are: [' +
+ keys(paths.strs).join(', ') +
+ ']'
+ : '`. The wildcard ids currently available are logged above.');
+ } else {
+ msg =
+ 'Multiple objects were found for an `' +
+ depType +
+ '` of a callback that only takes one value. The id spec is ' +
+ JSON.stringify(spec.id) +
+ (anyVals ? ' with MATCH values ' + anyVals : '') +
+ ' and the property is `' +
+ spec.property +
+ '`. The objects we found are: ' +
+ JSON.stringify(map(pick(['id', 'property']), idProps));
+ }
+ }
+ return [idProps[0], msg];
+}
+
+function fillVals(
+ paths: any,
+ layout: any,
+ cb: ICallback,
+ specs: any,
+ depType: any,
+ allowAllMissing: boolean = false
+) {
+ const getter = depType === 'Input' ? cb.getInputs : cb.getState;
+ const errors: any[] = [];
+ let emptyMultiValues = 0;
+
+ const inputVals = getter(paths).map((inputList: any, i: number) => {
+ const [inputs, inputError] = unwrapIfNotMulti(
+ paths,
+ inputList.map(({ id, property, path: path_ }: any) => ({
+ id,
+ property,
+ value: (path(path_, layout) as any).props[property]
+ })),
+ specs[i],
+ cb.anyVals,
+ depType
+ );
+ if (isMultiValued(specs[i]) && !inputs.length) {
+ emptyMultiValues++;
+ }
+ if (inputError) {
+ errors.push(inputError);
+ }
+ return inputs;
+ });
+
+ if (errors.length) {
+ if (
+ allowAllMissing &&
+ errors.length + emptyMultiValues === inputVals.length
+ ) {
+ // We have at least one non-multivalued input, but all simple and
+ // multi-valued inputs are missing.
+ // (if all inputs are multivalued and all missing we still return
+ // them as normal, and fire the callback.)
+ return null;
+ }
+ // If we get here we have some missing and some present inputs.
+ // Or all missing in a context that doesn't allow this.
+ // That's a real problem, so throw the first message as an error.
+ refErr(errors, paths);
+ }
+
+ return inputVals;
+}
+
+function refErr(errors: any, paths: any) {
+ const err = errors[0];
+ if (err.indexOf('logged above') !== -1) {
+ // Wildcard reference errors mention a list of wildcard specs logged
+ // TODO: unwrapped list of wildcard ids?
+ // eslint-disable-next-line no-console
+ console.error(paths.objs);
+ }
+ throw new ReferenceError(err);
+}
+
+const getVals = (input: any) =>
+ Array.isArray(input) ? pluck('value', input) : input.value;
+
+const zipIfArray = (a: any, b: any) => (Array.isArray(a) ? zip(a, b) : [[a, b]]);
+
+function handleClientside(clientside_function: any, payload: ICallbackPayload) {
+ const dc = ((window as any).dash_clientside = (window as any).dash_clientside || {});
+ if (!dc.no_update) {
+ Object.defineProperty(dc, 'no_update', {
+ value: { description: 'Return to prevent updating an Output.' },
+ writable: false
+ });
+
+ Object.defineProperty(dc, 'PreventUpdate', {
+ value: { description: 'Throw to prevent updating all Outputs.' },
+ writable: false
+ });
+ }
+
+ const { inputs, outputs, state } = payload;
+
+ let returnValue;
+
+ try {
+ const { namespace, function_name } = clientside_function;
+ let args = inputs.map(getVals);
+ if (state) {
+ args = concat(args, state.map(getVals));
+ }
+
+ // setup callback context
+ const input_dict = inputsToDict(inputs);
+ dc.callback_context = {};
+ dc.callback_context.triggered = payload.changedPropIds.map(prop_id => ({
+ prop_id: prop_id,
+ value: input_dict[prop_id]
+ }));
+ dc.callback_context.inputs_list = inputs;
+ dc.callback_context.inputs = input_dict;
+ dc.callback_context.states_list = state;
+ dc.callback_context.states = inputsToDict(state);
+
+ returnValue = dc[namespace][function_name](...args);
+ } catch (e) {
+ if (e === dc.PreventUpdate) {
+ return {};
+ }
+ throw e;
+ } finally {
+ delete dc.callback_context;
+ }
+
+ if (typeof returnValue?.then === 'function') {
+ throw new Error(
+ 'The clientside function returned a Promise. ' +
+ 'Promises are not supported in Dash clientside ' +
+ 'right now, but may be in the future.'
+ );
+ }
+
+ const data: any = {};
+ zipIfArray(outputs, returnValue).forEach(([outi, reti]) => {
+ zipIfArray(outi, reti).forEach(([outij, retij]) => {
+ const { id, property } = outij;
+ const idStr = stringifyId(id);
+ const dataForId = (data[idStr] = data[idStr] || {});
+ if (retij !== dc.no_update) {
+ dataForId[property] = retij;
+ }
+ });
+ });
+ return data;
+}
+
+function handleServerside(
+ hooks: any,
+ config: any,
+ payload: any
+): Promise {
+ if (hooks.request_pre !== null) {
+ hooks.request_pre(payload);
+ }
+
+ return fetch(
+ `${urlBase(config)}_dash-update-component`,
+ mergeDeepRight(config.fetch, {
+ method: 'POST',
+ headers: getCSRFHeader() as any,
+ body: JSON.stringify(payload)
+ })
+ ).then((res: any) => {
+ const { status } = res;
+ if (status === STATUS.OK) {
+ return res.json().then((data: any) => {
+ const { multi, response } = data;
+ if (hooks.request_post !== null) {
+ hooks.request_post(payload, response);
+ }
+
+ if (multi) {
+ return response;
+ }
+
+ const { output } = payload;
+ const id = output.substr(0, output.lastIndexOf('.'));
+ return { [id]: response.props };
+ });
+ }
+ if (status === STATUS.PREVENT_UPDATE) {
+ return {};
+ }
+ throw res;
+ }, () => {
+ // fetch rejection - this means the request didn't return,
+ // we don't get here from 400/500 errors, only network
+ // errors or unresponsive servers.
+ throw new Error('Callback failed: the server did not respond.');
+ });
+}
+
+function inputsToDict(inputs_list: any) {
+ // Ported directly from _utils.py, inputs_to_dict
+ // takes an array of inputs (some inputs may be an array)
+ // returns an Object (map):
+ // keys of the form `id.property` or `{"id": 0}.property`
+ // values contain the property value
+ if (!inputs_list) {
+ return {};
+ }
+ const inputs: any = {};
+ for (let i = 0; i < inputs_list.length; i++) {
+ if (Array.isArray(inputs_list[i])) {
+ const inputsi = inputs_list[i];
+ for (let ii = 0; ii < inputsi.length; ii++) {
+ const id_str = `${stringifyId(inputsi[ii].id)}.${
+ inputsi[ii].property
+ }`;
+ inputs[id_str] = inputsi[ii].value ?? null;
+ }
+ } else {
+ const id_str = `${stringifyId(inputs_list[i].id)}.${
+ inputs_list[i].property
+ }`;
+ inputs[id_str] = inputs_list[i].value ?? null;
+ }
+ }
+ return inputs;
+}
+
+export function executeCallback(
+ cb: IPrioritizedCallback,
+ config: any,
+ hooks: any,
+ paths: any,
+ layout: any,
+ { allOutputs }: any
+): IExecutingCallback {
+ const { output, inputs, state, clientside_function } = cb.callback;
+
+ try {
+ const inVals = fillVals(paths, layout, cb, inputs, 'Input', true);
+
+ /* Prevent callback if there's no inputs */
+ if (inVals === null) {
+ return {
+ ...cb,
+ executionPromise: null
+ };
+ }
+
+ const outputs: any[] = [];
+ const outputErrors: any[] = [];
+ allOutputs.forEach((out: any, i: number) => {
+ const [outi, erri] = unwrapIfNotMulti(
+ paths,
+ map(pick(['id', 'property']), out),
+ cb.callback.outputs[i],
+ cb.anyVals,
+ 'Output'
+ );
+ outputs.push(outi);
+ if (erri) {
+ outputErrors.push(erri);
+ }
+ });
+
+ if (outputErrors.length) {
+ if (flatten(inVals).length) {
+ refErr(outputErrors, paths);
+ }
+ // This case is all-empty multivalued wildcard inputs,
+ // which we would normally fire the callback for, except
+ // some outputs are missing. So instead we treat it like
+ // regular missing inputs and just silently prevent it.
+ return {
+ ...cb,
+ executionPromise: null
+ };
+ }
+
+ const __promise = new Promise(resolve => {
+ try {
+ const payload: ICallbackPayload = {
+ output,
+ outputs: isMultiOutputProp(output) ? outputs : outputs[0],
+ inputs: inVals,
+ changedPropIds: keys(cb.changedPropIds),
+ state: cb.callback.state.length ?
+ fillVals(paths, layout, cb, state, 'State') :
+ undefined
+ };
+
+ if (clientside_function) {
+ try {
+ resolve({ data: handleClientside(clientside_function, payload), payload });
+ } catch (error) {
+ resolve({ error, payload });
+ }
+ return null;
+ } else {
+ handleServerside(hooks, config, payload)
+ .then(data => resolve({ data, payload }))
+ .catch(error => resolve({ error, payload }));
+ }
+ } catch (error) {
+ resolve({ error, payload: null });
+ }
+ });
+
+ const newCb = {
+ ...cb,
+ executionPromise: __promise
+ };
+
+ return newCb;
+ } catch (error) {
+ return {
+ ...cb,
+ executionPromise: { error, payload: null }
+ };
+ }
+}
diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js
index add6a20597..1d77086d1d 100644
--- a/dash-renderer/src/actions/dependencies.js
+++ b/dash-renderer/src/actions/dependencies.js
@@ -5,9 +5,7 @@ import {
any,
ap,
assoc,
- clone,
difference,
- dissoc,
equals,
evolve,
findIndex,
@@ -18,24 +16,25 @@ import {
isEmpty,
keys,
map,
- mergeDeepRight,
mergeRight,
- mergeWith,
- partition,
path,
- pickBy,
pluck,
- propEq,
props,
startsWith,
- unnest,
values,
zip,
zipObj,
} from 'ramda';
-const mergeMax = mergeWith(Math.max);
-
+import {
+ combineIdAndProp,
+ getCallbacksByInput,
+ getPriority,
+ INDIRECT,
+ mergeMax,
+ makeResolvedCallback,
+ resolveDeps,
+} from './dependencies_ts';
import {computePaths, getPath} from './paths';
import {crawlLayout} from './utils';
@@ -91,7 +90,7 @@ function parseMultipleOutputs(outputIdAndProp) {
return outputIdAndProp.substr(2, outputIdAndProp.length - 4).split('...');
}
-function splitIdAndProp(idAndProp) {
+export function splitIdAndProp(idAndProp) {
// since wildcard ids can have . in them but props can't,
// look for the last . in the string and split there
const dotPos = idAndProp.lastIndexOf('.');
@@ -109,9 +108,6 @@ export function parseIfWildcard(idStr) {
return isWildcardId(idStr) ? parseWildcardId(idStr) : idStr;
}
-export const combineIdAndProp = ({id, property}) =>
- `${stringifyId(id)}.${property}`;
-
/*
* JSON.stringify - for the object form - but ensuring keys are sorted
*/
@@ -846,7 +842,14 @@ function findWildcardKeys(id) {
* Optionally, include another reference set of the same - to ensure the
* correct matching of MATCH or ALLSMALLER between input and output items.
*/
-function idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) {
+export function idMatch(
+ keys,
+ vals,
+ patternVals,
+ refKeys,
+ refVals,
+ refPatternVals
+) {
for (let i = 0; i < keys.length; i++) {
const val = vals[i];
const patternVal = patternVals[i];
@@ -901,74 +904,6 @@ function getAnyVals(patternVals, vals) {
return matches.length ? JSON.stringify(matches) : '';
}
-function resolveDeps(refKeys, refVals, refPatternVals) {
- return paths => ({id: idPattern, property}) => {
- if (typeof idPattern === 'string') {
- const path = getPath(paths, idPattern);
- return path ? [{id: idPattern, property, path}] : [];
- }
- const keys = Object.keys(idPattern).sort();
- const patternVals = props(keys, idPattern);
- const keyStr = keys.join(',');
- const keyPaths = paths.objs[keyStr];
- if (!keyPaths) {
- return [];
- }
- const result = [];
- keyPaths.forEach(({values: vals, path}) => {
- if (
- idMatch(
- keys,
- vals,
- patternVals,
- refKeys,
- refVals,
- refPatternVals
- )
- ) {
- result.push({id: zipObj(keys, vals), property, path});
- }
- });
- return result;
- };
-}
-
-/*
- * Create a pending callback object. Includes the original callback definition,
- * its resolved ID (including the value of all MATCH wildcards),
- * accessors to find all inputs, outputs, and state involved in this
- * callback (lazy as not all users will want all of these),
- * placeholders for which other callbacks this one is blockedBy or blocking,
- * and a boolean for whether it has been dispatched yet.
- */
-const makeResolvedCallback = (callback, resolve, anyVals) => ({
- callback,
- anyVals,
- resolvedId: callback.output + anyVals,
- getOutputs: paths => callback.outputs.map(resolve(paths)),
- getInputs: paths => callback.inputs.map(resolve(paths)),
- getState: paths => callback.state.map(resolve(paths)),
- blockedBy: {},
- blocking: {},
- changedPropIds: {},
- initialCall: false,
- requestId: 0,
- requestedOutputs: {},
-});
-
-const DIRECT = 2;
-const INDIRECT = 1;
-
-let nextRequestId = 0;
-
-/*
- * Give a callback a new requestId.
- */
-export function setNewRequestId(callback) {
- nextRequestId++;
- return assoc('requestId', nextRequestId, callback);
-}
-
/*
* Does this item (input / output / state) support multiple values?
* string IDs do not; wildcard IDs only do if they contain ALL or ALLSMALLER
@@ -991,8 +926,6 @@ export function isMultiValued({id}) {
* The result is a list of {id (string or object), property (string)}
* getInputs: same for inputs
* getState: same for state
- * blockedBy: an object of {[resolvedId]: 1} blocking this callback
- * blocking: an object of {[resolvedId]: 1} this callback is blocking
* changedPropIds: an object of {[idAndProp]: v} triggering this callback
* v = DIRECT (2): the prop was changed in the front end, so dependent
* callbacks *MUST* be executed.
@@ -1003,12 +936,6 @@ export function isMultiValued({id}) {
* this value on page load or changing part of the layout.
* By default this is true for callbacks generated by
* getCallbackByOutput, false from getCallbacksByInput.
- * requestId: integer: starts at 0. when this callback is dispatched it will
- * get a unique requestId, but if it gets added again the requestId will
- * be reset to 0, and we'll know to ignore the response of the first
- * request.
- * requestedOutputs: object of {[idStr]: [props]} listing all the props
- * actually requested for update.
* }
*/
function getCallbackByOutput(graphs, paths, id, prop) {
@@ -1062,7 +989,7 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) {
});
}
-function addAllResolvedFromOutputs(resolve, paths, matches) {
+export function addAllResolvedFromOutputs(resolve, paths, matches) {
return callback => {
const {matchKeys, firstSingleOutput, outputs} = callback;
if (matchKeys.length) {
@@ -1119,47 +1046,6 @@ function addAllResolvedFromOutputs(resolve, paths, matches) {
* (with an MATCH corresponding to the input's ALLSMALLER) will only appear
* in one entry.
*/
-export function getCallbacksByInput(graphs, paths, id, prop, changeType) {
- const matches = [];
- const idAndProp = combineIdAndProp({id, property: prop});
-
- if (typeof id === 'string') {
- // standard id version
- const callbacks = (graphs.inputMap[id] || {})[prop];
- if (!callbacks) {
- return [];
- }
-
- callbacks.forEach(
- addAllResolvedFromOutputs(resolveDeps(), paths, matches)
- );
- } else {
- // wildcard version
- const keys = Object.keys(id).sort();
- const vals = props(keys, id);
- const keyStr = keys.join(',');
- const patterns = (graphs.inputPatterns[keyStr] || {})[prop];
- if (!patterns) {
- return [];
- }
- patterns.forEach(pattern => {
- if (idMatch(keys, vals, pattern.values)) {
- pattern.callbacks.forEach(
- addAllResolvedFromOutputs(
- resolveDeps(keys, vals, pattern.values),
- paths,
- matches
- )
- );
- }
- });
- }
- matches.forEach(match => {
- match.changedPropIds[idAndProp] = changeType || DIRECT;
- });
- return matches;
-}
-
export function getWatchedKeys(id, newProps, graphs) {
if (!(id && graphs && newProps.length)) {
return [];
@@ -1205,7 +1091,7 @@ export function getWatchedKeys(id, newProps, graphs) {
* {callback, resolvedId, getOutputs, getInputs, getState, ...etc}
* See getCallbackByOutput for details.
*/
-export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
+export function getUnfilteredLayoutCallbacks(graphs, paths, layoutChunk, opts) {
const {outputsOnly, removedArrayInputsOnly, newPaths, chunkPath} = opts;
const foundCbIds = {};
const callbacks = [];
@@ -1315,245 +1201,11 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) {
}
});
- // We still need to follow these forward in order to capture blocks and,
- // if based on a partial layout, any knock-on effects in the full layout.
- const finalCallbacks = followForward(graphs, paths, callbacks);
-
- // Exception to the `initialCall` case of callbacks found by output:
- // if *every* input to this callback is itself an output of another
- // callback earlier in the chain, we remove the `initialCall` flag
- // so that if all of those prior callbacks abort all of their outputs,
- // this later callback never runs.
- // See test inin003 "callback2 is never triggered, even on initial load"
- finalCallbacks.forEach(cb => {
- if (cb.initialCall && !isEmpty(cb.blockedBy)) {
- const inputs = flatten(cb.getInputs(paths));
- cb.initialCall = false;
- inputs.forEach(i => {
- const propId = combineIdAndProp(i);
- if (cb.changedPropIds[propId]) {
- cb.changedPropIds[propId] = INDIRECT;
- } else {
- cb.initialCall = true;
- }
- });
- }
- });
-
- return finalCallbacks;
-}
-
-export function removePendingCallback(
- pendingCallbacks,
- paths,
- removeResolvedId,
- skippedProps
-) {
- const finalPendingCallbacks = [];
- pendingCallbacks.forEach(pending => {
- const {blockedBy, blocking, changedPropIds, resolvedId} = pending;
- if (resolvedId !== removeResolvedId) {
- finalPendingCallbacks.push(
- mergeRight(pending, {
- blockedBy: dissoc(removeResolvedId, blockedBy),
- blocking: dissoc(removeResolvedId, blocking),
- changedPropIds: pickBy(
- (v, k) => v === DIRECT || !includes(k, skippedProps),
- changedPropIds
- ),
- })
- );
- }
- });
- // If any callback no longer has any changed inputs, it shouldn't fire.
- // This will repeat recursively until all unneeded callbacks are pruned
- if (skippedProps.length) {
- for (let i = 0; i < finalPendingCallbacks.length; i++) {
- const cb = finalPendingCallbacks[i];
- if (!cb.initialCall && isEmpty(cb.changedPropIds)) {
- return removePendingCallback(
- finalPendingCallbacks,
- paths,
- cb.resolvedId,
- flatten(cb.getOutputs(paths)).map(combineIdAndProp)
- );
- }
- }
- }
- return finalPendingCallbacks;
-}
-
-/*
- * Split the list of pending callbacks into ready (not blocked by any others)
- * and blocked. Sort the ready callbacks by how many each is blocking, on the
- * theory that the most important ones to dispatch are the ones with the most
- * others depending on them.
- */
-export function findReadyCallbacks(pendingCallbacks) {
- const [readyCallbacks, blockedCallbacks] = partition(
- pending => isEmpty(pending.blockedBy) && !pending.requestId,
- pendingCallbacks
+ return map(
+ cb => ({
+ ...cb,
+ priority: getPriority(graphs, paths, cb),
+ }),
+ callbacks
);
- readyCallbacks.sort((a, b) => {
- return Object.keys(b.blocking).length - Object.keys(a.blocking).length;
- });
-
- return {readyCallbacks, blockedCallbacks};
-}
-
-function addBlock(callbacks, blockingId, blockedId) {
- callbacks.forEach(({blockedBy, blocking, resolvedId}) => {
- if (resolvedId === blockingId || blocking[blockingId]) {
- blocking[blockedId] = 1;
- } else if (resolvedId === blockedId || blockedBy[blockedId]) {
- blockedBy[blockingId] = 1;
- }
- });
-}
-
-function collectIds(callbacks) {
- const allResolvedIds = {};
- callbacks.forEach(({resolvedId}, i) => {
- allResolvedIds[resolvedId] = i;
- });
- return allResolvedIds;
-}
-
-/*
- * Take a list of callbacks and follow them all forward, ie see if any of their
- * outputs are inputs of another callback. Any new callbacks get added to the
- * list. All that come after another get marked as blocked by that one, whether
- * they were in the initial list or not.
- */
-export function followForward(graphs, paths, callbacks_) {
- const callbacks = clone(callbacks_);
- const allResolvedIds = collectIds(callbacks);
- let i;
- let callback;
-
- const followOutput = ({id, property}) => {
- const nextCBs = getCallbacksByInput(
- graphs,
- paths,
- id,
- property,
- INDIRECT
- );
- nextCBs.forEach(nextCB => {
- let existingIndex = allResolvedIds[nextCB.resolvedId];
- if (existingIndex === undefined) {
- existingIndex = callbacks.length;
- callbacks.push(nextCB);
- allResolvedIds[nextCB.resolvedId] = existingIndex;
- } else {
- const existingCB = callbacks[existingIndex];
- existingCB.changedPropIds = mergeMax(
- existingCB.changedPropIds,
- nextCB.changedPropIds
- );
- }
- addBlock(callbacks, callback.resolvedId, nextCB.resolvedId);
- });
- };
-
- // Using a for loop instead of forEach because followOutput may extend the
- // callbacks array, and we want to continue into these new elements.
- for (i = 0; i < callbacks.length; i++) {
- callback = callbacks[i];
- const outputs = unnest(callback.getOutputs(paths));
- outputs.forEach(followOutput);
- }
- return callbacks;
-}
-
-function mergeAllBlockers(cb1, cb2) {
- function mergeBlockers(a, b) {
- if (cb1[a][cb2.resolvedId] && !cb2[b][cb1.resolvedId]) {
- cb2[b][cb1.resolvedId] = cb1[a][cb2.resolvedId];
- cb2[b] = mergeMax(cb1[b], cb2[b]);
- cb1[a] = mergeMax(cb2[a], cb1[a]);
- }
- }
- mergeBlockers('blockedBy', 'blocking');
- mergeBlockers('blocking', 'blockedBy');
-}
-
-/*
- * Given two arrays of pending callbacks, merge them into one so that
- * each will only fire once, and any extra blockages from combining the lists
- * will be accounted for.
- */
-export function mergePendingCallbacks(cb1, cb2) {
- if (!cb2.length) {
- return cb1;
- }
- if (!cb1.length) {
- return cb2;
- }
- const finalCallbacks = clone(cb1);
- const callbacks2 = clone(cb2);
- const allResolvedIds = collectIds(finalCallbacks);
-
- callbacks2.forEach((callback, i) => {
- const existingIndex = allResolvedIds[callback.resolvedId];
- if (existingIndex !== undefined) {
- finalCallbacks.forEach(finalCb => {
- mergeAllBlockers(finalCb, callback);
- });
- callbacks2.slice(i + 1).forEach(cb2 => {
- mergeAllBlockers(cb2, callback);
- });
- finalCallbacks[existingIndex] = mergeDeepRight(
- finalCallbacks[existingIndex],
- callback
- );
- } else {
- allResolvedIds[callback.resolvedId] = finalCallbacks.length;
- finalCallbacks.push(callback);
- }
- });
-
- return finalCallbacks;
-}
-
-/*
- * Remove callbacks whose outputs or changed inputs have been removed
- * from the layout
- */
-export function pruneRemovedCallbacks(pendingCallbacks, paths) {
- const removeIds = [];
- let cleanedCallbacks = pendingCallbacks.map(callback => {
- const {changedPropIds, getOutputs, resolvedId} = callback;
- if (!flatten(getOutputs(paths)).length) {
- removeIds.push(resolvedId);
- return callback;
- }
-
- let omittedProps = false;
- const newChangedProps = pickBy((_, propId) => {
- if (getPath(paths, splitIdAndProp(propId).id)) {
- return true;
- }
- omittedProps = true;
- return false;
- }, changedPropIds);
-
- return omittedProps
- ? assoc('changedPropIds', newChangedProps, callback)
- : callback;
- });
-
- removeIds.forEach(resolvedId => {
- const cb = cleanedCallbacks.find(propEq('resolvedId', resolvedId));
- if (cb) {
- cleanedCallbacks = removePendingCallback(
- pendingCallbacks,
- paths,
- resolvedId,
- flatten(cb.getOutputs(paths)).map(combineIdAndProp)
- );
- }
- });
-
- return cleanedCallbacks;
}
diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts
new file mode 100644
index 0000000000..840f03d89f
--- /dev/null
+++ b/dash-renderer/src/actions/dependencies_ts.ts
@@ -0,0 +1,333 @@
+import {
+ all,
+ assoc,
+ concat,
+ difference,
+ filter,
+ flatten,
+ forEach,
+ isEmpty,
+ keys,
+ map,
+ mergeWith,
+ partition,
+ pickBy,
+ props,
+ reduce,
+ zipObj
+} from 'ramda';
+import { ICallback, ICallbackProperty, ICallbackDefinition, ILayoutCallbackProperty, ICallbackTemplate } from '../types/callbacks';
+import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getUnfilteredLayoutCallbacks, isMultiValued, idMatch } from './dependencies';
+import { getPath } from './paths';
+
+export const DIRECT = 2;
+export const INDIRECT = 1;
+export const mergeMax = mergeWith(Math.max);
+
+export const combineIdAndProp = ({
+ id,
+ property
+}: ICallbackProperty) => `${stringifyId(id)}.${property}`;
+
+export function getCallbacksByInput(
+ graphs: any,
+ paths: any,
+ id: any,
+ prop: any,
+ changeType?: any,
+ withPriority: boolean = true
+): ICallback[] {
+ const matches: ICallback[] = [];
+ const idAndProp = combineIdAndProp({ id, property: prop });
+
+ if (typeof id === 'string') {
+ // standard id version
+ const callbacks = (graphs.inputMap[id] || {})[prop];
+ if (!callbacks) {
+ return [];
+ }
+
+ callbacks.forEach(
+ addAllResolvedFromOutputs(resolveDeps(), paths, matches)
+ );
+ } else {
+ // wildcard version
+ const _keys = Object.keys(id).sort();
+ const vals = props(_keys, id);
+ const keyStr = _keys.join(',');
+ const patterns: any[] = (graphs.inputPatterns[keyStr] || {})[prop];
+ if (!patterns) {
+ return [];
+ }
+ patterns.forEach(pattern => {
+ if (idMatch(_keys, vals, pattern.values)) {
+ pattern.callbacks.forEach(
+ addAllResolvedFromOutputs(
+ resolveDeps(_keys, vals, pattern.values),
+ paths,
+ matches
+ )
+ );
+ }
+ });
+ }
+ matches.forEach(match => {
+ match.changedPropIds[idAndProp] = changeType || DIRECT;
+ if (withPriority) {
+ match.priority = getPriority(graphs, paths, match)
+ }
+ });
+ return matches;
+}
+
+/*
+ * Builds a tree of all callbacks that can be triggered by the provided callback.
+ * Uses the number of callbacks at each tree depth and the total depth of the tree
+ * to create a sortable priority hash.
+ */
+export function getPriority(graphs: any, paths: any, callback: ICallback): string {
+ let callbacks: ICallback[] = [callback];
+ let touchedOutputs: { [key: string]: boolean } = {};
+ let priority: number[] = [];
+
+ while (callbacks.length) {
+ const outputs = filter(
+ o => !touchedOutputs[combineIdAndProp(o)],
+ flatten(map(
+ cb => flatten(cb.getOutputs(paths)),
+ callbacks
+ ))
+ );
+
+ touchedOutputs = reduce(
+ (touched, o) => assoc(combineIdAndProp(o), true, touched),
+ touchedOutputs,
+ outputs
+ );
+
+ callbacks = flatten(map(
+ ({ id, property }: any) => getCallbacksByInput(
+ graphs,
+ paths,
+ id,
+ property,
+ INDIRECT,
+ false
+ ),
+ outputs
+ ));
+
+ if (callbacks.length) {
+ priority.push(callbacks.length);
+ }
+ }
+
+ priority.unshift(priority.length);
+
+ return map(i => Math.min(i, 35).toString(36), priority).join('');
+}
+
+export const getReadyCallbacks = (
+ paths: any,
+ candidates: ICallback[],
+ callbacks: ICallback[] = candidates
+): ICallback[] => {
+ // Skip if there's no candidates
+ if (!candidates.length) {
+ return [];
+ }
+
+ // Find all outputs of all active callbacks
+ const outputs = map(
+ combineIdAndProp,
+ reduce(
+ (o, cb) => concat(o, flatten(cb.getOutputs(paths))),
+ [],
+ callbacks
+ )
+ );
+
+ // Make `outputs` hash table for faster access
+ const outputsMap: { [key: string]: boolean } = {};
+ forEach(output => outputsMap[output] = true, outputs);
+
+ // Find `requested` callbacks that do not depend on a outstanding output (as either input or state)
+ return filter(
+ cb => all(
+ cbp => !outputsMap[combineIdAndProp(cbp)],
+ flatten(cb.getInputs(paths))
+ ),
+ candidates
+ );
+}
+
+export const getLayoutCallbacks = (
+ graphs: any,
+ paths: any,
+ layout: any,
+ options: any
+): ICallback[] => {
+ let exclusions: string[] = [];
+ let callbacks = getUnfilteredLayoutCallbacks(
+ graphs,
+ paths,
+ layout,
+ options
+ );
+
+ /*
+ Remove from the initial callbacks those that are left with only excluded inputs.
+
+ Exclusion of inputs happens when:
+ - an input is missing
+ - an input in the initial callback chain depends only on excluded inputs
+
+ Further execlusion might happen after callbacks return with:
+ - PreventUpdate
+ - no_update
+ */
+ while (true) {
+ // Find callbacks for which all inputs are missing or in the exclusions
+ const [included, excluded] = partition(({
+ callback: { inputs },
+ getInputs
+ }) => all(isMultiValued, inputs) ||
+ !isEmpty(difference(
+ map(combineIdAndProp, flatten(getInputs(paths))),
+ exclusions
+ )),
+ callbacks
+ );
+
+ // If there's no additional exclusions, break loop - callbacks have been cleaned
+ if (!excluded.length) {
+ break;
+ }
+
+ callbacks = included;
+
+ // update exclusions with all additional excluded outputs
+ exclusions = concat(
+ exclusions,
+ map(combineIdAndProp, flatten(map(
+ ({ getOutputs }) => getOutputs(paths),
+ excluded
+ )))
+ );
+ }
+
+ /*
+ Return all callbacks with an `executionGroup` to allow group-processing
+ */
+ const executionGroup = Math.random().toString(16);
+ return map(cb => ({
+ ...cb,
+ executionGroup
+ }), callbacks);
+}
+
+export const getUniqueIdentifier = ({
+ anyVals,
+ callback: {
+ inputs,
+ outputs,
+ state
+ }
+}: ICallback): string => concat(
+ map(combineIdAndProp, [
+ ...inputs,
+ ...outputs,
+ ...state
+ ]),
+ Array.isArray(anyVals) ?
+ anyVals :
+ anyVals === '' ? [] : [anyVals]
+ ).join(',');
+
+export function includeObservers(id: any, properties: any, graphs: any, paths: any): ICallback[] {
+ return flatten(map(
+ propName => getCallbacksByInput(graphs, paths, id, propName),
+ keys(properties)
+ ));
+}
+
+/*
+ * Create a pending callback object. Includes the original callback definition,
+ * its resolved ID (including the value of all MATCH wildcards),
+ * accessors to find all inputs, outputs, and state involved in this
+ * callback (lazy as not all users will want all of these).
+ */
+export const makeResolvedCallback = (
+ callback: ICallbackDefinition,
+ resolve: (_: any) => (_: ICallbackProperty) => ILayoutCallbackProperty[],
+ anyVals: any[] | string
+): ICallbackTemplate => ({
+ callback,
+ anyVals,
+ resolvedId: callback.output + anyVals,
+ getOutputs: paths => callback.outputs.map(resolve(paths)),
+ getInputs: paths => callback.inputs.map(resolve(paths)),
+ getState: paths => callback.state.map(resolve(paths)),
+ changedPropIds: {},
+ initialCall: false
+});
+
+export function pruneCallbacks(callbacks: T[], paths: any): {
+ added: T[],
+ removed: T[]
+} {
+ const [, removed] = partition(
+ ({ getOutputs, callback: { outputs } }) => flatten(getOutputs(paths)).length === outputs.length,
+ callbacks
+ );
+
+ const [, modified] = partition(
+ ({ getOutputs }) => !flatten(getOutputs(paths)).length,
+ removed
+ );
+
+ const added = map(
+ cb => assoc('changedPropIds', pickBy(
+ (_, propId) => getPath(paths, splitIdAndProp(propId).id),
+ cb.changedPropIds
+ ), cb),
+ modified
+ );
+
+ return {
+ added,
+ removed
+ };
+}
+
+export function resolveDeps(refKeys?: any, refVals?: any, refPatternVals?: string) {
+ return (paths: any) => ({ id: idPattern, property }: ICallbackProperty) => {
+ if (typeof idPattern === 'string') {
+ const path = getPath(paths, idPattern);
+ return path ? [{ id: idPattern, property, path }] : [];
+ }
+ const _keys = Object.keys(idPattern).sort();
+ const patternVals = props(_keys, idPattern);
+ const keyStr = _keys.join(',');
+ const keyPaths = paths.objs[keyStr];
+ if (!keyPaths) {
+ return [];
+ }
+ const result: ILayoutCallbackProperty[] = [];
+ keyPaths.forEach(({ values: vals, path }: any) => {
+ if (
+ idMatch(
+ _keys,
+ vals,
+ patternVals,
+ refKeys,
+ refVals,
+ refPatternVals
+ )
+ ) {
+ result.push({ id: zipObj(_keys, vals), property, path });
+ }
+ });
+ return result;
+ };
+}
diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js
index a48c81a687..7d165f41a6 100644
--- a/dash-renderer/src/actions/index.js
+++ b/dash-renderer/src/actions/index.js
@@ -1,59 +1,22 @@
-import {
- concat,
- flatten,
- has,
- isEmpty,
- keys,
- map,
- mergeDeepRight,
- once,
- path,
- pick,
- pickBy,
- pluck,
- propEq,
- type,
- uniq,
- without,
- zip,
-} from 'ramda';
+import {once} from 'ramda';
import {createAction} from 'redux-actions';
+import {addRequestedCallbacks} from './callbacks';
import {getAppState} from '../reducers/constants';
import {getAction} from './constants';
import cookie from 'cookie';
-import {urlBase} from './utils';
-import {
- combineIdAndProp,
- findReadyCallbacks,
- followForward,
- getCallbacksByInput,
- getCallbacksInLayout,
- isMultiOutputProp,
- isMultiValued,
- mergePendingCallbacks,
- removePendingCallback,
- parseIfWildcard,
- pruneRemovedCallbacks,
- setNewRequestId,
- stringifyId,
- validateCallbacksToLayout,
-} from './dependencies';
-import {computePaths, getPath} from './paths';
-import {STATUS} from '../constants/constants';
-import {applyPersistence, prunePersistence} from '../persistence';
+import {validateCallbacksToLayout} from './dependencies';
+import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
+import {getPath} from './paths';
-import isAppReady from './isAppReady';
-
-export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
-export const setPendingCallbacks = createAction('SET_PENDING_CALLBACKS');
-export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
-export const setGraphs = createAction(getAction('SET_GRAPHS'));
-export const setPaths = createAction(getAction('SET_PATHS'));
+export const onError = createAction(getAction('ON_ERROR'));
export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE'));
export const setConfig = createAction(getAction('SET_CONFIG'));
+export const setGraphs = createAction(getAction('SET_GRAPHS'));
export const setHooks = createAction(getAction('SET_HOOKS'));
export const setLayout = createAction(getAction('SET_LAYOUT'));
-export const onError = createAction(getAction('ON_ERROR'));
+export const setPaths = createAction(getAction('SET_PATHS'));
+export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE'));
+export const updateProps = createAction(getAction('ON_PROP_CHANGE'));
export const dispatchError = dispatch => (message, lines) =>
dispatch(
@@ -103,10 +66,13 @@ function triggerDefaultState(dispatch, getState) {
);
}
- const initialCallbacks = getCallbacksInLayout(graphs, paths, layout, {
- outputsOnly: true,
- });
- dispatch(startCallbacks(initialCallbacks));
+ dispatch(
+ addRequestedCallbacks(
+ getLayoutCallbacks(graphs, paths, layout, {
+ outputsOnly: true,
+ })
+ )
+ );
}
export const redo = moveHistory('REDO');
@@ -135,606 +101,15 @@ function moveHistory(changeType) {
};
}
-function unwrapIfNotMulti(paths, idProps, spec, anyVals, depType) {
- let msg = '';
-
- if (isMultiValued(spec)) {
- return [idProps, msg];
- }
-
- if (idProps.length !== 1) {
- if (!idProps.length) {
- const isStr = typeof spec.id === 'string';
- msg =
- 'A nonexistent object was used in an `' +
- depType +
- '` of a Dash callback. The id of this object is ' +
- (isStr
- ? '`' + spec.id + '`'
- : JSON.stringify(spec.id) +
- (anyVals ? ' with MATCH values ' + anyVals : '')) +
- ' and the property is `' +
- spec.property +
- (isStr
- ? '`. The string ids in the current layout are: [' +
- keys(paths.strs).join(', ') +
- ']'
- : '`. The wildcard ids currently available are logged above.');
- } else {
- msg =
- 'Multiple objects were found for an `' +
- depType +
- '` of a callback that only takes one value. The id spec is ' +
- JSON.stringify(spec.id) +
- (anyVals ? ' with MATCH values ' + anyVals : '') +
- ' and the property is `' +
- spec.property +
- '`. The objects we found are: ' +
- JSON.stringify(map(pick(['id', 'property']), idProps));
- }
- }
- return [idProps[0], msg];
-}
-
-function startCallbacks(callbacks) {
- return async function(dispatch, getState) {
- return await fireReadyCallbacks(dispatch, getState, callbacks);
- };
-}
-
-async function fireReadyCallbacks(dispatch, getState, callbacks) {
- const {readyCallbacks, blockedCallbacks} = findReadyCallbacks(callbacks);
- const {config, hooks, layout, paths} = getState();
-
- // We want to calculate all the outputs only once, but we need them
- // for pendingCallbacks which we're going to dispatch prior to
- // initiating the queue. So first loop over readyCallbacks to
- // generate the output lists, then dispatch pendingCallbacks,
- // then loop again to fire off the requests.
- const outputStash = {};
- const requestedCallbacks = readyCallbacks.map(cb => {
- const cbOut = setNewRequestId(cb);
-
- const {requestId, getOutputs} = cbOut;
- const allOutputs = getOutputs(paths);
- const flatOutputs = flatten(allOutputs);
- const allPropIds = [];
-
- const reqOut = {};
- flatOutputs.forEach(({id, property}) => {
- const idStr = stringifyId(id);
- const idOut = (reqOut[idStr] = reqOut[idStr] || []);
- idOut.push(property);
- allPropIds.push(combineIdAndProp({id: idStr, property}));
- });
- cbOut.requestedOutputs = reqOut;
-
- outputStash[requestId] = {allOutputs, allPropIds};
-
- return cbOut;
- });
-
- const allCallbacks = concat(requestedCallbacks, blockedCallbacks);
- dispatch(setPendingCallbacks(allCallbacks));
-
- const ids = requestedCallbacks.map(cb => [
- cb.getInputs(paths),
- cb.getState(paths),
- ]);
- await isAppReady(layout, paths, uniq(pluck('id', flatten(ids))));
-
- function fireNext() {
- return fireReadyCallbacks(
- dispatch,
- getState,
- getState().pendingCallbacks
- );
- }
-
- let hasClientSide = false;
-
- const queue = requestedCallbacks.map(cb => {
- const {output, inputs, state, clientside_function} = cb.callback;
- const {requestId, resolvedId} = cb;
- const {allOutputs, allPropIds} = outputStash[requestId];
-
- let payload;
- try {
- const inVals = fillVals(paths, layout, cb, inputs, 'Input', true);
-
- const preventCallback = () => {
- removeCallbackFromPending();
- // no server call here; for performance purposes pretend this is
- // a clientside callback and defer fireNext for the end
- // of the currently-ready callbacks.
- hasClientSide = true;
- return null;
- };
-
- if (inVals === null) {
- return preventCallback();
- }
-
- const outputs = [];
- const outputErrors = [];
- allOutputs.forEach((out, i) => {
- const [outi, erri] = unwrapIfNotMulti(
- paths,
- map(pick(['id', 'property']), out),
- cb.callback.outputs[i],
- cb.anyVals,
- 'Output'
- );
- outputs.push(outi);
- if (erri) {
- outputErrors.push(erri);
- }
- });
- if (outputErrors.length) {
- if (flatten(inVals).length) {
- refErr(outputErrors, paths);
- }
- // This case is all-empty multivalued wildcard inputs,
- // which we would normally fire the callback for, except
- // some outputs are missing. So instead we treat it like
- // regular missing inputs and just silently prevent it.
- return preventCallback();
- }
-
- payload = {
- output,
- outputs: isMultiOutputProp(output) ? outputs : outputs[0],
- inputs: inVals,
- changedPropIds: keys(cb.changedPropIds),
- };
- if (cb.callback.state.length) {
- payload.state = fillVals(paths, layout, cb, state, 'State');
- }
- } catch (e) {
- handleError(e);
- return fireNext();
- }
-
- function updatePending(pendingCallbacks, skippedProps) {
- const newPending = removePendingCallback(
- pendingCallbacks,
- getState().paths,
- resolvedId,
- skippedProps
- );
- dispatch(setPendingCallbacks(newPending));
- }
-
- function handleData(data) {
- let {pendingCallbacks} = getState();
- if (!requestIsActive(pendingCallbacks, resolvedId, requestId)) {
- return;
- }
- const updated = [];
- Object.entries(data).forEach(([id, props]) => {
- const parsedId = parseIfWildcard(id);
-
- const {layout: oldLayout, paths: oldPaths} = getState();
-
- const appliedProps = doUpdateProps(
- dispatch,
- getState,
- parsedId,
- props
- );
- if (appliedProps) {
- // doUpdateProps can cause new callbacks to be added
- // via derived props - update pendingCallbacks
- // But we may also need to merge in other callbacks that
- // we found in an earlier interation of the data loop.
- const statePendingCallbacks = getState().pendingCallbacks;
- if (statePendingCallbacks !== pendingCallbacks) {
- pendingCallbacks = mergePendingCallbacks(
- pendingCallbacks,
- statePendingCallbacks
- );
- }
-
- Object.keys(appliedProps).forEach(property => {
- updated.push(combineIdAndProp({id, property}));
- });
-
- if (has('children', appliedProps)) {
- const oldChildren = path(
- concat(getPath(oldPaths, parsedId), [
- 'props',
- 'children',
- ]),
- oldLayout
- );
- // If components changed, need to update paths,
- // check if all pending callbacks are still
- // valid, and add all callbacks associated with
- // new components, either as inputs or outputs,
- // or components removed from ALL/ALLSMALLER inputs
- pendingCallbacks = updateChildPaths(
- dispatch,
- getState,
- pendingCallbacks,
- parsedId,
- appliedProps.children,
- oldChildren
- );
- }
-
- // persistence edge case: if you explicitly update the
- // persistence key, other props may change that require us
- // to fire additional callbacks
- const addedProps = pickBy(
- (v, k) => !(k in props),
- appliedProps
- );
- if (!isEmpty(addedProps)) {
- const {graphs, paths} = getState();
- pendingCallbacks = includeObservers(
- id,
- addedProps,
- graphs,
- paths,
- pendingCallbacks
- );
- }
- }
- });
- updatePending(pendingCallbacks, without(updated, allPropIds));
- }
-
- function removeCallbackFromPending() {
- const {pendingCallbacks} = getState();
- if (requestIsActive(pendingCallbacks, resolvedId, requestId)) {
- // Skip all prop updates from this callback, and remove
- // it from the pending list so callbacks it was blocking
- // that have other changed inputs will still fire.
- updatePending(pendingCallbacks, allPropIds);
- }
- }
-
- function handleError(err) {
- removeCallbackFromPending();
- const outputs = payload
- ? map(combineIdAndProp, flatten([payload.outputs])).join(', ')
- : output;
- let message = `Callback error updating ${outputs}`;
- if (clientside_function) {
- const {namespace: ns, function_name: fn} = clientside_function;
- message += ` via clientside function ${ns}.${fn}`;
- }
- handleAsyncError(err, message, dispatch);
- }
-
- if (clientside_function) {
- try {
- handleData(handleClientside(clientside_function, payload));
- } catch (err) {
- handleError(err);
- }
- hasClientSide = true;
- return null;
- }
-
- return handleServerside(config, payload, hooks)
- .then(handleData)
- .catch(handleError)
- .then(fireNext);
- });
- const done = Promise.all(queue);
- return hasClientSide ? fireNext().then(done) : done;
-}
-
-function fillVals(paths, layout, cb, specs, depType, allowAllMissing) {
- const getter = depType === 'Input' ? cb.getInputs : cb.getState;
- const errors = [];
- let emptyMultiValues = 0;
-
- const inputVals = getter(paths).map((inputList, i) => {
- const [inputs, inputError] = unwrapIfNotMulti(
- paths,
- inputList.map(({id, property, path: path_}) => ({
- id,
- property,
- value: path(path_, layout).props[property],
- })),
- specs[i],
- cb.anyVals,
- depType
- );
- if (isMultiValued(specs[i]) && !inputs.length) {
- emptyMultiValues++;
- }
- if (inputError) {
- errors.push(inputError);
- }
- return inputs;
- });
-
- if (errors.length) {
- if (
- allowAllMissing &&
- errors.length + emptyMultiValues === inputVals.length
- ) {
- // We have at least one non-multivalued input, but all simple and
- // multi-valued inputs are missing.
- // (if all inputs are multivalued and all missing we still return
- // them as normal, and fire the callback.)
- return null;
- }
- // If we get here we have some missing and some present inputs.
- // Or all missing in a context that doesn't allow this.
- // That's a real problem, so throw the first message as an error.
- refErr(errors, paths);
- }
-
- return inputVals;
-}
-
-function refErr(errors, paths) {
- const err = errors[0];
- if (err.indexOf('logged above') !== -1) {
- // Wildcard reference errors mention a list of wildcard specs logged
- // TODO: unwrapped list of wildcard ids?
- // eslint-disable-next-line no-console
- console.error(paths.objs);
- }
- throw new ReferenceError(err);
-}
-
-function handleServerside(config, payload, hooks) {
- if (hooks.request_pre !== null) {
- hooks.request_pre(payload);
- }
-
- return fetch(
- `${urlBase(config)}_dash-update-component`,
- mergeDeepRight(config.fetch, {
- method: 'POST',
- headers: getCSRFHeader(),
- body: JSON.stringify(payload),
- })
- ).then(
- res => {
- const {status} = res;
- if (status === STATUS.OK) {
- return res.json().then(data => {
- const {multi, response} = data;
- if (hooks.request_post !== null) {
- hooks.request_post(payload, response);
- }
-
- if (multi) {
- return response;
- }
-
- const {output} = payload;
- const id = output.substr(0, output.lastIndexOf('.'));
- return {[id]: response.props};
- });
- }
- if (status === STATUS.PREVENT_UPDATE) {
- return {};
- }
- throw res;
- },
- () => {
- // fetch rejection - this means the request didn't return,
- // we don't get here from 400/500 errors, only network
- // errors or unresponsive servers.
- throw new Error('Callback failed: the server did not respond.');
- }
- );
-}
-
-const getVals = input =>
- Array.isArray(input) ? pluck('value', input) : input.value;
-
-const zipIfArray = (a, b) => (Array.isArray(a) ? zip(a, b) : [[a, b]]);
-
-function inputsToDict(inputs_list) {
- // Ported directly from _utils.py, inputs_to_dict
- // takes an array of inputs (some inputs may be an array)
- // returns an Object (map):
- // keys of the form `id.property` or `{"id": 0}.property`
- // values contain the property value
- if (!inputs_list) {
- return {};
- }
- const inputs = {};
- for (let i = 0; i < inputs_list.length; i++) {
- if (Array.isArray(inputs_list[i])) {
- const inputsi = inputs_list[i];
- for (let ii = 0; ii < inputsi.length; ii++) {
- const id_str = `${stringifyId(inputsi[ii].id)}.${
- inputsi[ii].property
- }`;
- inputs[id_str] = inputsi[ii].value ?? null;
- }
- } else {
- const id_str = `${stringifyId(inputs_list[i].id)}.${
- inputs_list[i].property
- }`;
- inputs[id_str] = inputs_list[i].value ?? null;
- }
- }
- return inputs;
-}
-
-function handleClientside(clientside_function, payload) {
- const dc = (window.dash_clientside = window.dash_clientside || {});
- if (!dc.no_update) {
- Object.defineProperty(dc, 'no_update', {
- value: {description: 'Return to prevent updating an Output.'},
- writable: false,
- });
-
- Object.defineProperty(dc, 'PreventUpdate', {
- value: {description: 'Throw to prevent updating all Outputs.'},
- writable: false,
- });
- }
-
- const {inputs, outputs, state} = payload;
-
- let returnValue;
-
- try {
- // setup callback context
- const input_dict = inputsToDict(inputs);
- dc.callback_context = {};
- dc.callback_context.triggered = payload.changedPropIds.map(prop_id => ({
- prop_id: prop_id,
- value: input_dict[prop_id],
- }));
- dc.callback_context.inputs_list = inputs;
- dc.callback_context.inputs = input_dict;
- dc.callback_context.states_list = state;
- dc.callback_context.states = inputsToDict(state);
-
- const {namespace, function_name} = clientside_function;
- let args = inputs.map(getVals);
- if (state) {
- args = concat(args, state.map(getVals));
- }
- returnValue = dc[namespace][function_name](...args);
-
- delete dc.callback_context;
- } catch (e) {
- if (e === dc.PreventUpdate) {
- return {};
- }
- throw e;
- }
-
- if (type(returnValue) === 'Promise') {
- throw new Error(
- 'The clientside function returned a Promise. ' +
- 'Promises are not supported in Dash clientside ' +
- 'right now, but may be in the future.'
- );
- }
-
- const data = {};
- zipIfArray(outputs, returnValue).forEach(([outi, reti]) => {
- zipIfArray(outi, reti).forEach(([outij, retij]) => {
- const {id, property} = outij;
- const idStr = stringifyId(id);
- const dataForId = (data[idStr] = data[idStr] || {});
- if (retij !== dc.no_update) {
- dataForId[property] = retij;
- }
- });
- });
- return data;
-}
-
-function requestIsActive(pendingCallbacks, resolvedId, requestId) {
- const thisCallback = pendingCallbacks.find(
- propEq('resolvedId', resolvedId)
- );
- // could be inactivated if it was requested again, in which case it could
- // potentially even have finished and been removed from the list
- return thisCallback && thisCallback.requestId === requestId;
-}
-
-function doUpdateProps(dispatch, getState, id, updatedProps) {
- const {layout, paths} = getState();
- const itempath = getPath(paths, id);
- if (!itempath) {
- return false;
- }
-
- // This is a callback-generated update.
- // Check if this invalidates existing persisted prop values,
- // or if persistence changed, whether this updates other props.
- const updatedProps2 = prunePersistence(
- path(itempath, layout),
- updatedProps,
- dispatch
- );
-
- // In case the update contains whole components, see if any of
- // those components have props to update to persist user edits.
- const {props} = applyPersistence({props: updatedProps2}, dispatch);
-
- dispatch(
- updateProps({
- itempath,
- props,
- source: 'response',
- })
- );
-
- return props;
-}
-
-function updateChildPaths(
- dispatch,
- getState,
- pendingCallbacks,
- id,
- children,
- oldChildren
-) {
- const {paths: oldPaths, graphs} = getState();
- const childrenPath = concat(getPath(oldPaths, id), ['props', 'children']);
- const paths = computePaths(children, childrenPath, oldPaths);
- dispatch(setPaths(paths));
-
- const cleanedCallbacks = pruneRemovedCallbacks(pendingCallbacks, paths);
-
- const newCallbacks = getCallbacksInLayout(graphs, paths, children, {
- chunkPath: childrenPath,
- });
-
- // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger
- // even due to the deletion of components
- const deletedComponentCallbacks = getCallbacksInLayout(
- graphs,
- oldPaths,
- oldChildren,
- {removedArrayInputsOnly: true, newPaths: paths, chunkPath: childrenPath}
- );
-
- const allNewCallbacks = mergePendingCallbacks(
- newCallbacks,
- deletedComponentCallbacks
- );
- return mergePendingCallbacks(cleanedCallbacks, allNewCallbacks);
-}
-
export function notifyObservers({id, props}) {
return async function(dispatch, getState) {
- const {graphs, paths, pendingCallbacks} = getState();
- const finalCallbacks = includeObservers(
- id,
- props,
- graphs,
- paths,
- pendingCallbacks
+ const {graphs, paths} = getState();
+ dispatch(
+ addRequestedCallbacks(includeObservers(id, props, graphs, paths))
);
- dispatch(startCallbacks(finalCallbacks));
};
}
-function includeObservers(id, props, graphs, paths, pendingCallbacks) {
- const changedProps = keys(props);
- let finalCallbacks = pendingCallbacks;
-
- changedProps.forEach(propName => {
- const newCBs = getCallbacksByInput(graphs, paths, id, propName);
- if (newCBs.length) {
- finalCallbacks = mergePendingCallbacks(
- finalCallbacks,
- followForward(graphs, paths, newCBs)
- );
- }
- });
- return finalCallbacks;
-}
-
export function handleAsyncError(err, message, dispatch) {
// Handle html error responses
if (err && typeof err.text === 'function') {
diff --git a/dash-renderer/src/actions/isLoading.ts b/dash-renderer/src/actions/isLoading.ts
new file mode 100644
index 0000000000..a501211317
--- /dev/null
+++ b/dash-renderer/src/actions/isLoading.ts
@@ -0,0 +1,5 @@
+import { createAction } from 'redux-actions';
+
+import { IsLoadingActionType, IsLoadingState } from '../reducers/isLoading';
+
+export const setIsLoading = createAction(IsLoadingActionType.Set);
diff --git a/dash-renderer/src/actions/loadingMap.ts b/dash-renderer/src/actions/loadingMap.ts
new file mode 100644
index 0000000000..2e4834dbd8
--- /dev/null
+++ b/dash-renderer/src/actions/loadingMap.ts
@@ -0,0 +1,5 @@
+import { createAction } from 'redux-actions';
+
+import { LoadingMapActionType, LoadingMapState } from '../reducers/loadingMap';
+
+export const setLoadingMap = createAction(LoadingMapActionType.Set);
diff --git a/dash-renderer/src/checkPropTypes.js b/dash-renderer/src/checkPropTypes.js
index 04f4d7b821..18dc8b5615 100644
--- a/dash-renderer/src/checkPropTypes.js
+++ b/dash-renderer/src/checkPropTypes.js
@@ -21,7 +21,7 @@ export default function checkPropTypes(
values,
location,
componentName,
- getStack
+ getStack = null
) {
const errors = [];
for (const typeSpecName in typeSpecs) {
diff --git a/dash-renderer/src/components/core/DocumentTitle.react.js b/dash-renderer/src/components/core/DocumentTitle.react.js
index 46eba06bfc..e192f32ce1 100644
--- a/dash-renderer/src/components/core/DocumentTitle.react.js
+++ b/dash-renderer/src/components/core/DocumentTitle.react.js
@@ -11,7 +11,7 @@ class DocumentTitle extends Component {
}
UNSAFE_componentWillReceiveProps(props) {
- if (props.pendingCallbacks.length) {
+ if (props.isLoading) {
document.title = 'Updating...';
} else {
document.title = this.state.initialTitle;
@@ -28,9 +28,9 @@ class DocumentTitle extends Component {
}
DocumentTitle.propTypes = {
- pendingCallbacks: PropTypes.array.isRequired,
+ isLoading: PropTypes.bool.isRequired,
};
export default connect(state => ({
- pendingCallbacks: state.pendingCallbacks,
+ isLoading: state.isLoading,
}))(DocumentTitle);
diff --git a/dash-renderer/src/components/core/Loading.react.js b/dash-renderer/src/components/core/Loading.react.js
index 999684a8dc..b4eb2793f5 100644
--- a/dash-renderer/src/components/core/Loading.react.js
+++ b/dash-renderer/src/components/core/Loading.react.js
@@ -3,16 +3,16 @@ import React from 'react';
import PropTypes from 'prop-types';
function Loading(props) {
- if (props.pendingCallbacks.length) {
+ if (props.isLoading) {
return ;
}
return null;
}
Loading.propTypes = {
- pendingCallbacks: PropTypes.array.isRequired,
+ isLoading: PropTypes.bool.isRequired,
};
export default connect(state => ({
- pendingCallbacks: state.pendingCallbacks,
+ isLoading: state.isLoading,
}))(Loading);
diff --git a/dash-renderer/src/components/error/ComponentErrorBoundary.react.js b/dash-renderer/src/components/error/ComponentErrorBoundary.react.js
index 5440cae0eb..f62b63cbe6 100644
--- a/dash-renderer/src/components/error/ComponentErrorBoundary.react.js
+++ b/dash-renderer/src/components/error/ComponentErrorBoundary.react.js
@@ -1,10 +1,8 @@
-import {connect} from 'react-redux';
import {Component} from 'react';
import PropTypes from 'prop-types';
-import Radium from 'radium';
import {onError, revert} from '../../actions';
-class UnconnectedComponentErrorBoundary extends Component {
+class ComponentErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
@@ -51,20 +49,11 @@ class UnconnectedComponentErrorBoundary extends Component {
}
}
-UnconnectedComponentErrorBoundary.propTypes = {
+ComponentErrorBoundary.propTypes = {
children: PropTypes.object,
componentId: PropTypes.string,
error: PropTypes.object,
dispatch: PropTypes.func,
};
-const ComponentErrorBoundary = connect(
- state => ({
- error: state.error,
- }),
- dispatch => {
- return {dispatch};
- }
-)(Radium(UnconnectedComponentErrorBoundary));
-
export default ComponentErrorBoundary;
diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts
new file mode 100644
index 0000000000..90e3787dd7
--- /dev/null
+++ b/dash-renderer/src/observers/executedCallbacks.ts
@@ -0,0 +1,237 @@
+import {
+ concat,
+ flatten,
+ isEmpty,
+ isNil,
+ map,
+ path,
+ forEach,
+ keys,
+ has,
+ pickBy,
+ toPairs
+} from 'ramda';
+
+import { IStoreState } from '../store';
+
+import {
+ aggregateCallbacks,
+ addRequestedCallbacks,
+ removeExecutedCallbacks,
+ addCompletedCallbacks,
+ addStoredCallbacks
+} from '../actions/callbacks';
+
+import { parseIfWildcard } from '../actions/dependencies';
+
+import {
+ combineIdAndProp,
+ getCallbacksByInput,
+ getLayoutCallbacks,
+ includeObservers
+} from '../actions/dependencies_ts';
+
+import {
+ ICallback,
+ IStoredCallback
+} from '../types/callbacks';
+
+import { updateProps, setPaths, handleAsyncError } from '../actions';
+import { getPath, computePaths } from '../actions/paths';
+
+import {
+ applyPersistence,
+ prunePersistence
+} from '../persistence';
+import { IStoreObserverDefinition } from '../StoreObserver';
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const {
+ callbacks: {
+ executed
+ }
+ } = getState();
+
+ function applyProps(id: any, updatedProps: any) {
+ const { layout, paths } = getState();
+ const itempath = getPath(paths, id);
+ if (!itempath) {
+ return false;
+ }
+
+ // This is a callback-generated update.
+ // Check if this invalidates existing persisted prop values,
+ // or if persistence changed, whether this updates other props.
+ updatedProps = prunePersistence(
+ path(itempath, layout),
+ updatedProps,
+ dispatch
+ );
+
+ // In case the update contains whole components, see if any of
+ // those components have props to update to persist user edits.
+ const { props } = applyPersistence({ props: updatedProps }, dispatch);
+
+ dispatch(
+ updateProps({
+ itempath,
+ props,
+ source: 'response'
+ })
+ );
+
+ return props;
+ }
+
+ let requestedCallbacks: ICallback[] = [];
+ let storedCallbacks: IStoredCallback[] = [];
+
+ forEach(cb => {
+ const predecessors = concat(
+ cb.predecessors ?? [],
+ [cb.callback]
+ );
+
+ const {
+ callback: {
+ clientside_function,
+ output
+ },
+ executionResult
+ } = cb;
+
+ if (isNil(executionResult)) {
+ return;
+ }
+
+ const { data, error, payload } = executionResult;
+
+ if (data !== undefined) {
+ forEach(([id, props]: [any, { [key: string]: any }]) => {
+ const parsedId = parseIfWildcard(id);
+ const { graphs, layout: oldLayout, paths: oldPaths } = getState();
+
+ // Components will trigger callbacks on their own as required (eg. derived)
+ const appliedProps = applyProps(parsedId, props);
+
+ // Add callbacks for modified inputs
+ requestedCallbacks = concat(
+ requestedCallbacks,
+ flatten(map(
+ prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop, true),
+ keys(props)
+ )).map(rcb => ({
+ ...rcb,
+ predecessors
+ }))
+ );
+
+ // New layout - trigger callbacks for that explicitly
+ if (has('children', appliedProps)) {
+ const { children } = appliedProps;
+
+ const oldChildrenPath: string[] = concat(getPath(oldPaths, parsedId) as string[], ['props', 'children']);
+ const oldChildren = path(oldChildrenPath, oldLayout);
+
+ const paths = computePaths(children, oldChildrenPath, oldPaths);
+ dispatch(setPaths(paths));
+
+ // Get callbacks for new layout (w/ execution group)
+ requestedCallbacks = concat(
+ requestedCallbacks,
+ getLayoutCallbacks(graphs, paths, children, {
+ chunkPath: oldChildrenPath
+ }).map(rcb => ({
+ ...rcb,
+ predecessors
+ }))
+ );
+
+ // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger
+ // even due to the deletion of components
+ requestedCallbacks = concat(
+ requestedCallbacks,
+ getLayoutCallbacks(graphs, oldPaths, oldChildren, {
+ removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath
+ }).map(rcb => ({
+ ...rcb,
+ predecessors
+ }))
+ );
+ }
+
+ // persistence edge case: if you explicitly update the
+ // persistence key, other props may change that require us
+ // to fire additional callbacks
+ const addedProps = pickBy(
+ (_, k) => !(k in props),
+ appliedProps
+ );
+ if (!isEmpty(addedProps)) {
+ const { graphs: currentGraphs, paths } = getState();
+
+ requestedCallbacks = concat(
+ requestedCallbacks,
+ includeObservers(id, addedProps, currentGraphs, paths).map(rcb => ({
+ ...rcb,
+ predecessors
+ }))
+ );
+ }
+ }, Object.entries(data));
+
+ // Add information about potentially updated outputs vs. updated outputs,
+ // this will be used to drop callbacks from execution groups when no output
+ // matching the downstream callback's inputs were modified
+ storedCallbacks.push({
+ ...cb,
+ executionMeta: {
+ allProps: map(combineIdAndProp, flatten(cb.getOutputs(getState().paths))),
+ updatedProps: flatten(map(
+ ([id, value]) => map(
+ property => combineIdAndProp({ id, property }),
+ keys(value)
+ ),
+ toPairs(data)
+ ))
+ }
+ });
+ }
+
+ if (error !== undefined) {
+ const outputs = payload
+ ? map(combineIdAndProp, flatten([payload.outputs])).join(', ')
+ : output;
+ let message = `Callback error updating ${outputs}`;
+ if (clientside_function) {
+ const { namespace: ns, function_name: fn } = clientside_function;
+ message += ` via clientside function ${ns}.${fn}`;
+ }
+
+ handleAsyncError(error, message, dispatch);
+
+ storedCallbacks.push({
+ ...cb,
+ executionMeta: {
+ allProps: map(combineIdAndProp, flatten(cb.getOutputs(getState().paths))),
+ updatedProps: []
+ }
+ });
+ }
+ }, executed);
+
+ dispatch(aggregateCallbacks([
+ executed.length ? removeExecutedCallbacks(executed) : null,
+ executed.length ? addCompletedCallbacks(executed.length) : null,
+ storedCallbacks.length ? addStoredCallbacks(storedCallbacks) : null,
+ requestedCallbacks.length ? addRequestedCallbacks(requestedCallbacks) : null
+ ]));
+ },
+ inputs: ['callbacks.executed']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/executingCallbacks.ts b/dash-renderer/src/observers/executingCallbacks.ts
new file mode 100644
index 0000000000..ffc92141e4
--- /dev/null
+++ b/dash-renderer/src/observers/executingCallbacks.ts
@@ -0,0 +1,63 @@
+import {
+ assoc,
+ find,
+ forEach,
+ partition
+} from 'ramda';
+
+import {
+ addExecutedCallbacks,
+ addWatchedCallbacks,
+ aggregateCallbacks,
+ removeExecutingCallbacks,
+ removeWatchedCallbacks
+} from '../actions/callbacks';
+
+import { IStoreObserverDefinition } from '../StoreObserver';
+import { IStoreState } from '../store';
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const {
+ callbacks: {
+ executing
+ }
+ } = getState();
+
+ const [deferred, skippedOrReady] = partition(cb => cb.executionPromise instanceof Promise, executing);
+
+ dispatch(aggregateCallbacks([
+ executing.length ? removeExecutingCallbacks(executing) : null,
+ deferred.length ? addWatchedCallbacks(deferred) : null,
+ skippedOrReady.length ? addExecutedCallbacks(skippedOrReady.map(cb => assoc('executionResult', cb.executionPromise as any, cb))) : null
+ ]));
+
+ forEach(async cb => {
+ const result = await cb.executionPromise;
+
+ const { callbacks: { watched } } = getState();
+
+ // Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled
+ // Find the callback instance or one that matches its promise (eg. could have been pruned)
+ const currentCb = find(_cb => _cb === cb || _cb.executionPromise === cb.executionPromise, watched);
+ if (!currentCb) {
+ return;
+ }
+
+ // Otherwise move to `executed` and remove from `watched`
+ dispatch(aggregateCallbacks([
+ removeWatchedCallbacks([currentCb]),
+ addExecutedCallbacks([{
+ ...currentCb,
+ executionResult: result
+ }])
+ ]));
+ }, deferred);
+ },
+ inputs: ['callbacks.executing']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/isLoading.ts b/dash-renderer/src/observers/isLoading.ts
new file mode 100644
index 0000000000..fc625d45a9
--- /dev/null
+++ b/dash-renderer/src/observers/isLoading.ts
@@ -0,0 +1,28 @@
+import { IStoreObserverDefinition } from '../StoreObserver';
+import { IStoreState } from '../store';
+import { getPendingCallbacks } from '../utils/callbacks';
+import { setIsLoading } from '../actions/isLoading';
+
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const {
+ callbacks,
+ isLoading
+ } = getState();
+
+ const pendingCallbacks = getPendingCallbacks(callbacks);
+
+ const next = Boolean(pendingCallbacks.length);
+
+ if (isLoading !== next) {
+ dispatch(setIsLoading(next));
+ }
+ },
+ inputs: ['callbacks']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts
new file mode 100644
index 0000000000..8242143083
--- /dev/null
+++ b/dash-renderer/src/observers/loadingMap.ts
@@ -0,0 +1,83 @@
+import {
+ equals,
+ flatten,
+ forEach,
+ isEmpty,
+ map,
+ reduce
+} from 'ramda';
+
+import { setLoadingMap } from '../actions/loadingMap';
+import { IStoreObserverDefinition } from '../StoreObserver';
+import { IStoreState } from '../store';
+import { ILayoutCallbackProperty } from '../types/callbacks';
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const {
+ callbacks: {
+ executing,
+ watched,
+ executed
+ },
+ loadingMap,
+ paths
+ } = getState();
+
+ /*
+ Get the path of all components impacted by callbacks
+ with states: executing, watched, executed.
+
+ For each path, keep track of all (id,prop) tuples that
+ are impacted for this node and nested nodes.
+ */
+
+ const loadingPaths: ILayoutCallbackProperty[] = flatten(map(
+ cb => cb.getOutputs(paths),
+ [...executing, ...watched, ...executed]
+ ));
+
+ const nextMap: any = isEmpty(loadingPaths) ?
+ null :
+ reduce(
+ (res, path) => {
+ let target = res;
+ const idprop = {
+ id: path.id,
+ property: path.property
+ };
+
+ // Assign all affected props for this path and nested paths
+ target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || [];
+ target.__dashprivate__idprops__.push(idprop);
+
+ forEach(p => {
+ target = (target[p] =
+ target[p] ??
+ p === 'children' ? [] : {}
+ )
+
+ target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || [];
+ target.__dashprivate__idprops__.push(idprop);
+ }, path.path);
+
+ // Assign one affected prop for this path
+ target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || idprop;
+
+ return res;
+ },
+ {} as any,
+ loadingPaths
+ );
+
+ if (!equals(nextMap, loadingMap)) {
+ dispatch(setLoadingMap(nextMap));
+ }
+ },
+ inputs: ['callbacks.executing', 'callbacks.watched', 'callbacks.executed']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts
new file mode 100644
index 0000000000..0b6efeb2ba
--- /dev/null
+++ b/dash-renderer/src/observers/prioritizedCallbacks.ts
@@ -0,0 +1,143 @@
+import {
+ find,
+ flatten,
+ forEach,
+ map,
+ partition,
+ pluck,
+ sort,
+ uniq
+} from 'ramda';
+
+import { IStoreState } from '../store';
+
+import {
+ addBlockedCallbacks,
+ addExecutingCallbacks,
+ aggregateCallbacks,
+ executeCallback,
+ removeBlockedCallbacks,
+ removePrioritizedCallbacks
+} from '../actions/callbacks';
+
+import { stringifyId } from '../actions/dependencies';
+
+import {
+ combineIdAndProp
+} from '../actions/dependencies_ts';
+
+import isAppReady from '../actions/isAppReady';
+
+import {
+ IBlockedCallback,
+ ICallback,
+ ILayoutCallbackProperty,
+ IPrioritizedCallback
+} from '../types/callbacks';
+import { IStoreObserverDefinition } from '../StoreObserver';
+
+const sortPriority = (c1: ICallback, c2: ICallback): number => {
+ return (c1.priority ?? '') > (c2.priority ?? '') ? -1 : 1;
+}
+
+const getStash = (cb: IPrioritizedCallback, paths: any): {
+ allOutputs: ILayoutCallbackProperty[][],
+ allPropIds: any[]
+} => {
+ const { getOutputs } = cb;
+ const allOutputs = getOutputs(paths);
+ const flatOutputs: any[] = flatten(allOutputs);
+ const allPropIds: any[] = [];
+
+ const reqOut: any = {};
+ flatOutputs.forEach(({ id, property }) => {
+ const idStr = stringifyId(id);
+ const idOut = (reqOut[idStr] = reqOut[idStr] || []);
+ idOut.push(property);
+ allPropIds.push(combineIdAndProp({ id: idStr, property }));
+ });
+
+ return { allOutputs, allPropIds };
+}
+
+const getIds = (cb: ICallback, paths: any) => uniq(pluck('id', [
+ ...flatten(cb.getInputs(paths)),
+ ...flatten(cb.getState(paths))
+]));
+
+const observer: IStoreObserverDefinition = {
+ observer: async ({
+ dispatch,
+ getState
+ }) => {
+ const { callbacks: { executing, watched }, config, hooks, layout, paths } = getState();
+ let { callbacks: { prioritized } } = getState();
+
+ const available = Math.max(
+ 0,
+ 12 - executing.length - watched.length
+ );
+
+ // Order prioritized callbacks based on depth and breadth of callback chain
+ prioritized = sort(sortPriority, prioritized);
+
+ // Divide between sync and async
+ const [syncCallbacks, asyncCallbacks] = partition(cb => isAppReady(
+ layout,
+ paths,
+ getIds(cb, paths)
+ ) === true, prioritized);
+
+ const pickedSyncCallbacks = syncCallbacks.slice(0, available);
+ const pickedAsyncCallbacks = asyncCallbacks.slice(0, available - pickedSyncCallbacks.length);
+
+ if (pickedSyncCallbacks.length) {
+ dispatch(aggregateCallbacks([
+ removePrioritizedCallbacks(pickedSyncCallbacks),
+ addExecutingCallbacks(map(
+ cb => executeCallback(cb, config, hooks, paths, layout, getStash(cb, paths)),
+ pickedSyncCallbacks
+ ))
+ ]));
+ }
+
+ if (pickedAsyncCallbacks.length) {
+ const deffered = map(
+ cb => ({
+ ...cb,
+ ...getStash(cb, paths),
+ isReady: isAppReady(layout, paths, getIds(cb, paths))
+ }),
+ pickedAsyncCallbacks
+ );
+
+ dispatch(aggregateCallbacks([
+ removePrioritizedCallbacks(pickedAsyncCallbacks),
+ addBlockedCallbacks(deffered)
+ ]));
+
+ forEach(async cb => {
+ await cb.isReady;
+
+ const { callbacks: { blocked } } = getState();
+
+ // Check if it's been removed from the `blocked` list since - on callback completion, another callback may be cancelled
+ // Find the callback instance or one that matches its promise (eg. could have been pruned)
+ const currentCb = find(_cb => _cb === cb || _cb.isReady === cb.isReady, blocked);
+ if (!currentCb) {
+ return;
+ }
+
+ const executingCallback = executeCallback(cb, config, hooks, paths, layout, cb);
+
+ dispatch(aggregateCallbacks([
+ removeBlockedCallbacks([cb]),
+ addExecutingCallbacks([executingCallback])
+ ]));
+ }, deffered);
+ }
+ },
+ inputs: ['callbacks.prioritized', 'callbacks.completed']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts
new file mode 100644
index 0000000000..1ca30a3ba1
--- /dev/null
+++ b/dash-renderer/src/observers/requestedCallbacks.ts
@@ -0,0 +1,348 @@
+import {
+ all,
+ concat,
+ difference,
+ filter,
+ flatten,
+ groupBy,
+ includes,
+ intersection,
+ isEmpty,
+ isNil,
+ map,
+ values
+} from 'ramda';
+
+import { IStoreState } from '../store';
+
+import {
+ aggregateCallbacks,
+ removeRequestedCallbacks,
+ removePrioritizedCallbacks,
+ removeExecutingCallbacks,
+ removeWatchedCallbacks,
+ addRequestedCallbacks,
+ addPrioritizedCallbacks,
+ addExecutingCallbacks,
+ addWatchedCallbacks,
+ removeBlockedCallbacks,
+ addBlockedCallbacks
+} from '../actions/callbacks';
+
+import { isMultiValued } from '../actions/dependencies';
+
+import {
+ combineIdAndProp,
+ getReadyCallbacks,
+ getUniqueIdentifier,
+ pruneCallbacks
+} from '../actions/dependencies_ts';
+
+import {
+ ICallback,
+ IExecutingCallback,
+ IStoredCallback,
+ IBlockedCallback
+} from '../types/callbacks';
+
+import { getPendingCallbacks } from '../utils/callbacks';
+import { IStoreObserverDefinition } from '../StoreObserver';
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const { callbacks, callbacks: { prioritized, blocked, executing, watched, stored }, paths } = getState();
+ let { callbacks: { requested } } = getState();
+
+ const pendingCallbacks = getPendingCallbacks(callbacks);
+
+ /*
+ 0. Prune circular callbacks that have completed the loop
+ - cb.callback included in cb.predecessors
+ */
+ const rCirculars = filter(
+ cb => includes(cb.callback, cb.predecessors ?? []),
+ requested
+ );
+
+ /*
+ TODO?
+ Clean up the `requested` list - during the dispatch phase,
+ circulars will be removed for real
+ */
+ requested = difference(requested, rCirculars);
+
+ /*
+ 1. Remove duplicated `requested` callbacks - give precedence to newer callbacks over older ones
+ */
+
+ /*
+ Extract all but the first callback from each IOS-key group
+ these callbacks are duplicates.
+ */
+ const rDuplicates = flatten(map(
+ group => group.slice(0, -1),
+ values(
+ groupBy(
+ getUniqueIdentifier,
+ requested
+ )
+ )
+ ));
+
+ /*
+ TODO?
+ Clean up the `requested` list - during the dispatch phase,
+ duplicates will be removed for real
+ */
+ requested = difference(requested, rDuplicates);
+
+ /*
+ 2. Remove duplicated `prioritized`, `executing` and `watching` callbacks
+ */
+
+ /*
+ Extract all but the first callback from each IOS-key group
+ these callbacks are `prioritized` and duplicates.
+ */
+ const pDuplicates = flatten(map(
+ group => group.slice(0, -1),
+ values(
+ groupBy(
+ getUniqueIdentifier,
+ concat(prioritized, requested)
+ )
+ )
+ ));
+
+ const bDuplicates = flatten(map(
+ group => group.slice(0, -1),
+ values(
+ groupBy(
+ getUniqueIdentifier,
+ concat(blocked, requested)
+ )
+ )
+ )) as IBlockedCallback[];
+
+ const eDuplicates = flatten(map(
+ group => group.slice(0, -1),
+ values(
+ groupBy(
+ getUniqueIdentifier,
+ concat(executing, requested)
+ )
+ )
+ )) as IExecutingCallback[];
+
+ const wDuplicates = flatten(map(
+ group => group.slice(0, -1),
+ values(
+ groupBy(
+ getUniqueIdentifier,
+ concat(watched, requested)
+ )
+ )
+ )) as IExecutingCallback[];
+
+ /*
+ 3. Modify or remove callbacks that are outputing to non-existing layout `id`.
+ */
+
+ const { added: rAdded, removed: rRemoved } = pruneCallbacks(requested, paths);
+ const { added: pAdded, removed: pRemoved } = pruneCallbacks(prioritized, paths);
+ const { added: bAdded, removed: bRemoved } = pruneCallbacks(blocked, paths);
+ const { added: eAdded, removed: eRemoved } = pruneCallbacks(executing, paths);
+ const { added: wAdded, removed: wRemoved } = pruneCallbacks(watched, paths);
+
+ /*
+ TODO?
+ Clean up the `requested` list - during the dispatch phase,
+ it will be updated for real
+ */
+ requested = concat(
+ difference(
+ requested,
+ rRemoved
+ ),
+ rAdded
+ );
+
+ /*
+ 4. Find `requested` callbacks that do not depend on a outstanding output (as either input or state)
+ */
+ let readyCallbacks = getReadyCallbacks(paths, requested, pendingCallbacks);
+
+ let oldBlocked: ICallback[] = [];
+ let newBlocked: ICallback[] = [];
+
+ /**
+ * If there is :
+ * - no ready callbacks
+ * - at least one requested callback
+ * - no additional pending callbacks
+ *
+ * can assume:
+ * - the requested callbacks are part of a circular dependency loop
+ *
+ * then recursively:
+ * - assume the first callback in the list is ready (the entry point for the loop)
+ * - check what callbacks are blocked / ready with the assumption
+ * - update the missing predecessors based on assumptions
+ * - continue until there are no remaining candidates
+ *
+ */
+ if (
+ !readyCallbacks.length &&
+ requested.length &&
+ requested.length === pendingCallbacks.length
+ ) {
+ let candidates = requested.slice(0);
+
+ while (candidates.length) {
+ // Assume 1st callback is ready and
+ // update candidates / readyCallbacks accordingly
+ const readyCallback = candidates[0];
+
+ readyCallbacks.push(readyCallback);
+ candidates = candidates.slice(1);
+
+ // Remaining candidates are not blocked by current assumptions
+ candidates = getReadyCallbacks(paths, candidates, readyCallbacks);
+
+ // Blocked requests need to make sure they have the callback as a predecessor
+ const blockedByAssumptions = difference(candidates, candidates);
+
+ const modified = filter(
+ cb => !cb.predecessors || !includes(readyCallback.callback, cb.predecessors),
+ blockedByAssumptions
+ );
+
+ oldBlocked = concat(oldBlocked, modified);
+ newBlocked = concat(newBlocked, modified.map(cb => ({
+ ...cb,
+ predecessors: concat(cb.predecessors ?? [], [readyCallback.callback])
+ })));
+ }
+ }
+
+ /*
+ TODO?
+ Clean up the `requested` list - during the dispatch phase,
+ it will be updated for real
+ */
+ requested = concat(
+ difference(
+ requested,
+ oldBlocked
+ ),
+ newBlocked
+ );
+
+ /*
+ 5. Prune callbacks that became irrelevant in their `executionGroup`
+ */
+
+ // Group by executionGroup, drop non-executionGroup callbacks
+ // those were not triggered by layout changes and don't have "strong" interdependency for
+ // callback chain completion
+ const pendingGroups = groupBy(
+ cb => cb.executionGroup as any,
+ filter(cb => !isNil(cb.executionGroup), stored)
+ );
+
+ const dropped: ICallback[] = filter(cb => {
+ // If there is no `stored` callback for the group, no outputs were dropped -> `cb` is kept
+ if (!cb.executionGroup || !pendingGroups[cb.executionGroup] || !pendingGroups[cb.executionGroup].length) {
+ return false;
+ }
+
+ // Get all intputs for `cb`
+ const inputs = map(combineIdAndProp, flatten(cb.getInputs(paths)));
+
+ // Get all the potentially updated props for the group so far
+ const allProps = flatten(map(
+ gcb => gcb.executionMeta.allProps,
+ pendingGroups[cb.executionGroup]
+ ));
+
+ // Get all the updated props for the group so far
+ const updated = flatten(map(
+ gcb => gcb.executionMeta.updatedProps,
+ pendingGroups[cb.executionGroup]
+ ));
+
+ // If there's no overlap between the updated props and the inputs,
+ // + there's no props that aren't covered by the potentially updated props,
+ // and not all inputs are multi valued
+ // -> drop `cb`
+ const res =
+ isEmpty(intersection(
+ inputs,
+ updated
+ )) &&
+ isEmpty(difference(
+ inputs,
+ allProps
+ ))
+ && !all(
+ isMultiValued,
+ cb.callback.inputs
+ );
+
+ return res;
+ },
+ readyCallbacks
+ );
+
+ /*
+ TODO?
+ Clean up the `requested` list - during the dispatch phase,
+ it will be updated for real
+ */
+ requested = difference(
+ requested,
+ dropped
+ );
+
+ readyCallbacks = difference(
+ readyCallbacks,
+ dropped
+ );
+
+ dispatch(aggregateCallbacks([
+ // Clean up duplicated callbacks
+ rDuplicates.length ? removeRequestedCallbacks(rDuplicates) : null,
+ pDuplicates.length ? removePrioritizedCallbacks(pDuplicates) : null,
+ bDuplicates.length ? removeBlockedCallbacks(bDuplicates) : null,
+ eDuplicates.length ? removeExecutingCallbacks(eDuplicates) : null,
+ wDuplicates.length ? removeWatchedCallbacks(wDuplicates) : null,
+ // Prune callbacks
+ rRemoved.length ? removeRequestedCallbacks(rRemoved) : null,
+ rAdded.length ? addRequestedCallbacks(rAdded) : null,
+ pRemoved.length ? removePrioritizedCallbacks(pRemoved) : null,
+ pAdded.length ? addPrioritizedCallbacks(pAdded) : null,
+ bRemoved.length ? removeBlockedCallbacks(bRemoved) : null,
+ bAdded.length ? addBlockedCallbacks(bAdded) : null,
+ eRemoved.length ? removeExecutingCallbacks(eRemoved) : null,
+ eAdded.length ? addExecutingCallbacks(eAdded) : null,
+ wRemoved.length ? removeWatchedCallbacks(wRemoved) : null,
+ wAdded.length ? addWatchedCallbacks(wAdded) : null,
+ // Prune circular callbacks
+ rCirculars.length ? removeRequestedCallbacks(rCirculars) : null,
+ // Prune circular assumptions
+ oldBlocked.length ? removeRequestedCallbacks(oldBlocked) : null,
+ newBlocked.length ? addRequestedCallbacks(newBlocked) : null,
+ // Drop non-triggered initial callbacks
+ dropped.length ? removeRequestedCallbacks(dropped) : null,
+ // Promote callbacks
+ readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null,
+ readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null
+ ]));
+ },
+ inputs: ['callbacks.requested', 'callbacks.completed']
+};
+
+export default observer;
diff --git a/dash-renderer/src/observers/storedCallbacks.ts b/dash-renderer/src/observers/storedCallbacks.ts
new file mode 100644
index 0000000000..83b19518b9
--- /dev/null
+++ b/dash-renderer/src/observers/storedCallbacks.ts
@@ -0,0 +1,69 @@
+import {
+ concat,
+ filter,
+ groupBy,
+ isNil,
+ partition,
+ reduce,
+ toPairs
+} from 'ramda';
+
+import { IStoreState } from '../store';
+
+import {
+ aggregateCallbacks,
+ removeStoredCallbacks
+} from '../actions/callbacks';
+
+import {
+ ICallback,
+ IStoredCallback
+} from '../types/callbacks';
+
+import { getPendingCallbacks } from '../utils/callbacks';
+import { IStoreObserverDefinition } from '../StoreObserver';
+
+const observer: IStoreObserverDefinition = {
+ observer: ({
+ dispatch,
+ getState
+ }) => {
+ const { callbacks } = getState();
+ const pendingCallbacks = getPendingCallbacks(callbacks);
+
+ let { callbacks: { stored } } = getState();
+
+ const [nullGroupCallbacks, groupCallbacks] = partition(
+ cb => isNil(cb.executionGroup),
+ stored
+ );
+
+ const executionGroups = groupBy(
+ cb => cb.executionGroup as any,
+ groupCallbacks
+ )
+
+ const pendingGroups = groupBy(
+ cb => cb.executionGroup as any,
+ filter(cb => !isNil(cb.executionGroup), pendingCallbacks)
+ );
+
+ let dropped = reduce((res, [
+ executionGroup,
+ executionGroupCallbacks
+ ]) => !pendingGroups[executionGroup] ?
+ concat(res, executionGroupCallbacks) :
+ res,
+ [] as IStoredCallback[],
+ toPairs(executionGroups)
+ );
+
+ dispatch(aggregateCallbacks([
+ nullGroupCallbacks.length ? removeStoredCallbacks(nullGroupCallbacks) : null,
+ dropped.length ? removeStoredCallbacks(dropped) : null
+ ]));
+ },
+ inputs: ['callbacks.stored', 'callbacks.completed']
+};
+
+export default observer;
diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts
new file mode 100644
index 0000000000..81d24e65ab
--- /dev/null
+++ b/dash-renderer/src/reducers/callbacks.ts
@@ -0,0 +1,154 @@
+import {
+ concat,
+ difference,
+ reduce
+} from 'ramda';
+
+import {
+ ICallback,
+ IExecutedCallback,
+ IExecutingCallback,
+ IStoredCallback,
+ IPrioritizedCallback,
+ IBlockedCallback,
+ IWatchedCallback
+} from '../types/callbacks';
+
+export enum CallbackActionType {
+ AddBlocked = 'Callbacks.AddBlocked',
+ AddExecuted = 'Callbacks.AddExecuted',
+ AddExecuting = 'Callbacks.AddExecuting',
+ AddPrioritized = 'Callbacks.AddPrioritized',
+ AddRequested = 'Callbacks.AddRequested',
+ AddStored = 'Callbacks.AddStored',
+ AddWatched = 'Callbacks.AddWatched',
+ RemoveBlocked = 'Callbacks.RemoveBlocked',
+ RemoveExecuted = 'Callbacks.RemoveExecuted',
+ RemoveExecuting = 'Callbacks.RemoveExecuting',
+ RemovePrioritized = 'Callbacks.ReomvePrioritized',
+ RemoveRequested = 'Callbacks.RemoveRequested',
+ RemoveStored = 'Callbacks.RemoveStored',
+ RemoveWatched = 'Callbacks.RemoveWatched'
+}
+
+export enum CallbackAggregateActionType {
+ AddCompleted = 'Callbacks.Completed',
+ Aggregate = 'Callbacks.Aggregate'
+}
+
+export interface IAggregateAction {
+ type: CallbackAggregateActionType.Aggregate,
+ payload: (ICallbackAction | ICompletedAction | null)[]
+}
+
+export interface ICallbackAction {
+ type: CallbackActionType;
+ payload: ICallback[];
+}
+
+export interface ICompletedAction {
+ type: CallbackAggregateActionType.AddCompleted,
+ payload: number
+}
+
+type CallbackAction =
+ IAggregateAction |
+ ICallbackAction |
+ ICompletedAction;
+
+export interface ICallbacksState {
+ requested: ICallback[];
+ prioritized: IPrioritizedCallback[];
+ blocked: IBlockedCallback[];
+ executing: IExecutingCallback[];
+ watched: IWatchedCallback[];
+ executed: IExecutedCallback[];
+ stored: IStoredCallback[];
+ completed: number;
+}
+
+const DEFAULT_STATE: ICallbacksState = {
+ blocked: [],
+ executed: [],
+ executing: [],
+ prioritized: [],
+ requested: [],
+ stored: [],
+ watched: [],
+ completed: 0
+};
+
+const transforms: {
+ [key: string]: (a1: ICallback[], a2: ICallback[]) => ICallback[]
+} = {
+ [CallbackActionType.AddBlocked]: concat,
+ [CallbackActionType.AddExecuted]: concat,
+ [CallbackActionType.AddExecuting]: concat,
+ [CallbackActionType.AddPrioritized]: concat,
+ [CallbackActionType.AddRequested]: concat,
+ [CallbackActionType.AddStored]: concat,
+ [CallbackActionType.AddWatched]: concat,
+ [CallbackActionType.RemoveBlocked]: difference,
+ [CallbackActionType.RemoveExecuted]: difference,
+ [CallbackActionType.RemoveExecuting]: difference,
+ [CallbackActionType.RemovePrioritized]: difference,
+ [CallbackActionType.RemoveRequested]: difference,
+ [CallbackActionType.RemoveStored]: difference,
+ [CallbackActionType.RemoveWatched]: difference
+};
+
+const fields: {
+ [key: string]: keyof Omit
+} = {
+ [CallbackActionType.AddBlocked]: 'blocked',
+ [CallbackActionType.AddExecuted]: 'executed',
+ [CallbackActionType.AddExecuting]: 'executing',
+ [CallbackActionType.AddPrioritized]: 'prioritized',
+ [CallbackActionType.AddRequested]: 'requested',
+ [CallbackActionType.AddStored]: 'stored',
+ [CallbackActionType.AddWatched]: 'watched',
+ [CallbackActionType.RemoveBlocked]: 'blocked',
+ [CallbackActionType.RemoveExecuted]: 'executed',
+ [CallbackActionType.RemoveExecuting]: 'executing',
+ [CallbackActionType.RemovePrioritized]: 'prioritized',
+ [CallbackActionType.RemoveRequested]: 'requested',
+ [CallbackActionType.RemoveStored]: 'stored',
+ [CallbackActionType.RemoveWatched]: 'watched'
+}
+
+const mutateCompleted = (
+ state: ICallbacksState,
+ action: ICompletedAction
+) => ({ ...state, completed: state.completed + action.payload });
+
+const mutateCallbacks = (
+ state: ICallbacksState,
+ action: ICallbackAction
+) => {
+ const transform = transforms[action.type];
+ const field = fields[action.type];
+
+ return (!transform || !field || action.payload.length === 0) ?
+ state : {
+ ...state,
+ [field]: transform(state[field], action.payload)
+ };
+}
+
+
+
+export default (
+ state: ICallbacksState = DEFAULT_STATE,
+ action: CallbackAction
+) => reduce((s, a) => {
+ if (a === null) {
+ return s;
+ } else if (a.type === CallbackAggregateActionType.AddCompleted) {
+ return mutateCompleted(s, a);
+ } else {
+ return mutateCallbacks(s, a);
+ }
+}, state, action.type === CallbackAggregateActionType.Aggregate ?
+ action.payload :
+ [action]
+);
diff --git a/dash-renderer/src/reducers/isLoading.ts b/dash-renderer/src/reducers/isLoading.ts
new file mode 100644
index 0000000000..0a25260551
--- /dev/null
+++ b/dash-renderer/src/reducers/isLoading.ts
@@ -0,0 +1,22 @@
+export enum IsLoadingActionType {
+ Set = 'IsLoading.Set'
+}
+
+export interface ILoadingMapAction {
+ type: IsLoadingActionType.Set;
+ payload: any;
+}
+
+type IsLoadingState = boolean;
+export {
+ IsLoadingState
+};
+
+const DEFAULT_STATE: IsLoadingState = true;
+
+export default (
+ state: IsLoadingState = DEFAULT_STATE,
+ action: ILoadingMapAction
+) => action.type === IsLoadingActionType.Set ?
+ action.payload :
+ state;
diff --git a/dash-renderer/src/reducers/loadingMap.ts b/dash-renderer/src/reducers/loadingMap.ts
new file mode 100644
index 0000000000..1fb31a208d
--- /dev/null
+++ b/dash-renderer/src/reducers/loadingMap.ts
@@ -0,0 +1,22 @@
+export enum LoadingMapActionType {
+ Set = 'LoadingMap.Set'
+}
+
+export interface ILoadingMapAction {
+ type: LoadingMapActionType.Set;
+ payload: any;
+}
+
+type LoadingMapState = any;
+export {
+ LoadingMapState
+};
+
+const DEFAULT_STATE: LoadingMapState = {};
+
+export default (
+ state: LoadingMapState = DEFAULT_STATE,
+ action: ILoadingMapAction
+) => action.type === LoadingMapActionType.Set ?
+ action.payload :
+ state;
diff --git a/dash-renderer/src/reducers/pendingCallbacks.js b/dash-renderer/src/reducers/pendingCallbacks.js
deleted file mode 100644
index 70a2cd3f86..0000000000
--- a/dash-renderer/src/reducers/pendingCallbacks.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const pendingCallbacks = (state = [], action) => {
- switch (action.type) {
- case 'SET_PENDING_CALLBACKS':
- return action.payload;
-
- default:
- return state;
- }
-};
-
-export default pendingCallbacks;
diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js
index ffdb8794fa..d238b75225 100644
--- a/dash-renderer/src/reducers/reducer.js
+++ b/dash-renderer/src/reducers/reducer.js
@@ -1,18 +1,20 @@
import {forEach, isEmpty, keys, path} from 'ramda';
import {combineReducers} from 'redux';
-import {getCallbacksByInput} from '../actions/dependencies';
+import {getCallbacksByInput} from '../actions/dependencies_ts';
-import layout from './layout';
-import graphs from './dependencyGraph';
-import paths from './paths';
-import pendingCallbacks from './pendingCallbacks';
+import createApiReducer from './api';
import appLifecycle from './appLifecycle';
-import history from './history';
+import callbacks from './callbacks';
+import config from './config';
+import graphs from './dependencyGraph';
import error from './error';
+import history from './history';
import hooks from './hooks';
-import createApiReducer from './api';
-import config from './config';
+import isLoading from './isLoading';
+import layout from './layout';
+import loadingMap from './loadingMap';
+import paths from './paths';
export const apiRequests = [
'dependenciesRequest',
@@ -24,14 +26,16 @@ export const apiRequests = [
function mainReducer() {
const parts = {
appLifecycle,
- layout,
- graphs,
- paths,
- pendingCallbacks,
+ callbacks,
config,
- history,
error,
+ graphs,
+ history,
hooks,
+ isLoading,
+ layout,
+ loadingMap,
+ paths,
};
forEach(r => {
parts[r] = createApiReducer(r);
diff --git a/dash-renderer/src/store.js b/dash-renderer/src/store.js
deleted file mode 100644
index fc9fa7f465..0000000000
--- a/dash-renderer/src/store.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import {createStore, applyMiddleware} from 'redux';
-import thunk from 'redux-thunk';
-import {createReducer} from './reducers/reducer';
-
-let store;
-
-/**
- * Initialize a Redux store with thunk, plus logging (only in development mode) middleware
- *
- * @param {bool} reset: discard any previous store
- *
- * @returns {Store}
- * An initialized redux store with middleware and possible hot reloading of reducers
- */
-const initializeStore = reset => {
- if (store && !reset) {
- return store;
- }
-
- const reducer = createReducer();
-
- // eslint-disable-next-line no-process-env
- if (process.env.NODE_ENV === 'production') {
- store = createStore(reducer, applyMiddleware(thunk));
- } else {
- // only attach logger to middleware in non-production mode
- const reduxDTEC = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
- if (reduxDTEC) {
- store = createStore(reducer, reduxDTEC(applyMiddleware(thunk)));
- } else {
- store = createStore(reducer, applyMiddleware(thunk));
- }
- }
-
- if (!reset) {
- // TODO - Protect this under a debug mode?
- window.store = store;
- }
-
- if (module.hot) {
- // Enable hot module replacement for reducers
- module.hot.accept('./reducers/reducer', () => {
- const nextRootReducer = require('./reducers/reducer').createReducer();
-
- store.replaceReducer(nextRootReducer);
- });
- }
-
- return store;
-};
-
-export default initializeStore;
diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts
new file mode 100644
index 0000000000..3b26f75a92
--- /dev/null
+++ b/dash-renderer/src/store.ts
@@ -0,0 +1,96 @@
+import { once } from 'ramda';
+import { createStore, applyMiddleware, Store, Observer } from 'redux';
+import thunk from 'redux-thunk';
+import {createReducer} from './reducers/reducer';
+import StoreObserver from './StoreObserver';
+import { ICallbacksState } from './reducers/callbacks';
+import { LoadingMapState } from './reducers/loadingMap';
+import { IsLoadingState } from './reducers/isLoading';
+
+import executedCallbacks from './observers/executedCallbacks';
+import executingCallbacks from './observers/executingCallbacks';
+import isLoading from './observers/isLoading'
+import loadingMap from './observers/loadingMap';
+import prioritizedCallbacks from './observers/prioritizedCallbacks';
+import requestedCallbacks from './observers/requestedCallbacks';
+import storedCallbacks from './observers/storedCallbacks';
+
+export interface IStoreObserver {
+ observer: Observer>;
+ inputs: string[];
+}
+
+export interface IStoreState {
+ callbacks: ICallbacksState;
+ isLoading: IsLoadingState;
+ loadingMap: LoadingMapState;
+ [key: string]: any;
+}
+
+let store: Store;
+const storeObserver = new StoreObserver();
+
+const setObservers = once(() => {
+ const observe = storeObserver.observe;
+
+ observe(isLoading);
+ observe(loadingMap);
+ observe(requestedCallbacks);
+ observe(prioritizedCallbacks);
+ observe(executingCallbacks);
+ observe(executedCallbacks);
+ observe(storedCallbacks);
+});
+
+function createAppStore(reducer: any, middleware: any) {
+ store = createStore(reducer, middleware);
+ storeObserver.setStore(store);
+ setObservers();
+}
+
+/**
+ * Initialize a Redux store with thunk, plus logging (only in development mode) middleware
+ *
+ * @param {bool} reset: discard any previous store
+ *
+ * @returns {Store}
+ * An initialized redux store with middleware and possible hot reloading of reducers
+ */
+const initializeStore = (reset?: boolean): Store => {
+ if (store && !reset) {
+ return store;
+ }
+
+ const reducer = createReducer();
+
+ // eslint-disable-next-line no-process-env
+ if (process.env.NODE_ENV === 'production') {
+ createAppStore(reducer, applyMiddleware(thunk));
+ } else {
+ // only attach logger to middleware in non-production mode
+ const reduxDTEC = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
+ if (reduxDTEC) {
+ createAppStore(reducer, reduxDTEC(applyMiddleware(thunk)));
+ } else {
+ createAppStore(reducer, applyMiddleware(thunk));
+ }
+ }
+
+ if (!reset) {
+ // TODO - Protect this under a debug mode?
+ (window as any).store = store;
+ }
+
+ if ((module as any).hot) {
+ // Enable hot module replacement for reducers
+ (module as any).hot.accept('./reducers/reducer', () => {
+ const nextRootReducer = require('./reducers/reducer').createReducer();
+
+ store.replaceReducer(nextRootReducer);
+ });
+ }
+
+ return store;
+};
+
+export default initializeStore;
diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts
new file mode 100644
index 0000000000..14882607d5
--- /dev/null
+++ b/dash-renderer/src/types/callbacks.ts
@@ -0,0 +1,85 @@
+type CallbackId = string | { [key: string]: any }
+
+export interface ICallbackDefinition {
+ clientside_function?: {
+ namespace: string;
+ function_name: string;
+ };
+ input: string;
+ inputs: ICallbackProperty[];
+ output: string;
+ outputs: ICallbackProperty[];
+ prevent_initial_call: boolean;
+ state: ICallbackProperty[];
+}
+
+export interface ICallbackProperty {
+ id: CallbackId;
+ property: string;
+}
+
+export interface ILayoutCallbackProperty extends ICallbackProperty {
+ path: (string | number)[];
+}
+
+export interface ICallbackTemplate {
+ anyVals: any[] | string;
+ callback: ICallbackDefinition;
+ changedPropIds: any;
+ executionGroup?: string;
+ initialCall: boolean;
+ getInputs: (paths: any) => ILayoutCallbackProperty[][];
+ getOutputs: (paths: any) => ILayoutCallbackProperty[][];
+ getState: (paths: any) => ILayoutCallbackProperty[][];
+ resolvedId: any;
+}
+
+export interface ICallback extends ICallbackTemplate {
+ predecessors?: ICallbackDefinition[];
+ priority?: string;
+}
+
+// tslint:disable-next-line:no-empty-interface
+export interface IPrioritizedCallback extends ICallback {
+
+}
+
+export interface IBlockedCallback extends IPrioritizedCallback {
+ allOutputs: ILayoutCallbackProperty[][];
+ allPropIds: any[];
+ isReady: Promise | true;
+}
+
+export interface IExecutingCallback extends IPrioritizedCallback {
+ executionPromise: Promise | CallbackResult | null;
+}
+
+// tslint:disable-next-line:no-empty-interface
+export interface IWatchedCallback extends IExecutingCallback {
+
+}
+
+export interface IExecutedCallback extends IWatchedCallback {
+ executionResult: CallbackResult | null;
+}
+
+export interface IStoredCallback extends IExecutedCallback {
+ executionMeta: {
+ allProps: string[];
+ updatedProps: string[];
+ }
+}
+
+export interface ICallbackPayload {
+ changedPropIds: any[];
+ inputs: any[];
+ output: string;
+ outputs: any[];
+ state?: any[] | null;
+}
+
+export type CallbackResult = {
+ data?: any;
+ error?: Error;
+ payload: ICallbackPayload | null;
+}
diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts
new file mode 100644
index 0000000000..cf9df689c1
--- /dev/null
+++ b/dash-renderer/src/utils/TreeContainer.ts
@@ -0,0 +1,78 @@
+import { path, type, has } from 'ramda';
+
+import Registry from '../registry';
+import { stringifyId } from '../actions/dependencies';
+
+function isLoadingComponent(layout: any) {
+ validateComponent(layout);
+ return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent;
+}
+
+const NULL_LOADING_STATE = false;
+
+export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) {
+ if (!loadingMap) {
+ return NULL_LOADING_STATE;
+ }
+
+ const loadingFragment: any = path(componentPath, loadingMap);
+ // Component and children are not loading if there's no loading fragment
+ // for the component's path in the layout.
+ if (!loadingFragment) {
+ return NULL_LOADING_STATE;
+ }
+
+ const idprop: any = loadingFragment.__dashprivate__idprop__;
+ if (idprop) {
+ return {
+ is_loading: true,
+ prop_name: idprop.property,
+ component_name: stringifyId(idprop.id)
+ };
+ }
+
+ const idprops: any = loadingFragment.__dashprivate__idprops__?.[0];
+ if (idprops && isLoadingComponent(componentLayout)) {
+ return {
+ is_loading: true,
+ prop_name: idprops.property,
+ component_name: stringifyId(idprops.id)
+ };
+ }
+
+ return NULL_LOADING_STATE;
+}
+
+export const getLoadingHash = (
+ componentPath: any,
+ loadingMap: any
+) => (
+ ((loadingMap && (path(componentPath, loadingMap) as any)?.__dashprivate__idprops__) ?? []) as any[]
+).map(({ id, property }) => `${id}.${property}`).join(',');
+
+export function validateComponent(componentDefinition: any) {
+ if (type(componentDefinition) === 'Array') {
+ throw new Error(
+ 'The children property of a component is a list of lists, instead ' +
+ 'of just a list. ' +
+ 'Check the component that has the following contents, ' +
+ 'and remove one of the levels of nesting: \n' +
+ JSON.stringify(componentDefinition, null, 2)
+ );
+ }
+ if (
+ type(componentDefinition) === 'Object' &&
+ !(
+ has('namespace', componentDefinition) &&
+ has('type', componentDefinition) &&
+ has('props', componentDefinition)
+ )
+ ) {
+ throw new Error(
+ 'An object was provided as `children` instead of a component, ' +
+ 'string, or number (or list of those). ' +
+ 'Check the children property that looks something like:\n' +
+ JSON.stringify(componentDefinition, null, 2)
+ );
+ }
+}
diff --git a/dash-renderer/src/utils/callbacks.ts b/dash-renderer/src/utils/callbacks.ts
new file mode 100644
index 0000000000..14befb6e7c
--- /dev/null
+++ b/dash-renderer/src/utils/callbacks.ts
@@ -0,0 +1,8 @@
+import { omit, values } from 'ramda';
+
+import { ICallbacksState } from '../reducers/callbacks';
+import { ICallback } from '../types/callbacks';
+
+export const getPendingCallbacks = (state: ICallbacksState) => Array().concat(
+ ...values(omit(['stored', 'completed'], state))
+);
diff --git a/dash-renderer/tsconfig.json b/dash-renderer/tsconfig.json
new file mode 100644
index 0000000000..ca13e15e1c
--- /dev/null
+++ b/dash-renderer/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true,
+ "emitDecoratorMetadata": false,
+ "experimentalDecorators": true,
+ "jsx": "react",
+ "lib": ["esnext", "dom", "es2018.promise"],
+ "module": "esnext",
+ "moduleResolution": "node",
+ "noImplicitAny": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "sourceMap": false,
+ "strict": true,
+ "strictBindCallApply": true,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": true,
+ "target": "esnext",
+ "traceResolution": false
+ },
+ "include": [
+ "src/*"
+ ]
+}
\ No newline at end of file
diff --git a/dash-renderer/tslint.json b/dash-renderer/tslint.json
new file mode 100644
index 0000000000..600dce2dfc
--- /dev/null
+++ b/dash-renderer/tslint.json
@@ -0,0 +1,57 @@
+{
+ "defaultSeverity": "error",
+ "extends": [
+ "tslint:recommended"
+ ],
+ "linterOptions": {
+ "exclude": [
+ ".config/**",
+ "cypress/**",
+ "inst/**",
+ "node_modules/**",
+ "@Types/**",
+ "venv/**",
+ "**/*.js"
+ ]
+ },
+ "rules": {
+ "array-type": false,
+ "arrow-parens": [true, "ban-single-arg-parens"],
+ "ban-types": false,
+ "eofline": true,
+ "max-classes-per-file": false,
+ "max-line-length": false,
+ "member-access": false,
+ "member-ordering": false,
+ "no-conditional-assignment": false,
+ "no-console": false,
+ "no-empty": false,
+ "no-unused-expression": [true, "allow-new"],
+ "object-literal-key-quotes": [true, "as-needed"],
+ "object-literal-sort-keys": false,
+ "object-literal-shorthand": false,
+ "one-line": [true,
+ "check-catch",
+ "check-finally",
+ "check-else",
+ "check-whitespace"
+ ],
+ "only-arrow-functions": [
+ true,
+ "allow-declarations",
+ "allow-named-functions"
+ ],
+ "ordered-imports": false,
+ "prefer-const": false,
+ "prefer-for-of": false,
+ "quotemark": [true, "single"],
+ "space-before-function-paren": [false, "always"],
+ "trailing-comma": [true, {
+ "singleline": "never",
+ "multiline": "never"
+ }],
+ "unified-signatures": false,
+ "variable-name": false
+ },
+ "rulesDirectory": []
+}
diff --git a/dash-renderer/webpack.config.js b/dash-renderer/webpack.config.js
index 7df554cd08..ae4ac37a5e 100644
--- a/dash-renderer/webpack.config.js
+++ b/dash-renderer/webpack.config.js
@@ -15,6 +15,11 @@ const defaults = {
loader: 'babel-loader',
},
},
+ {
+ test: /\.ts(x?)$/,
+ exclude: /node_modules/,
+ use: ['babel-loader', 'ts-loader'],
+ },
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
@@ -24,6 +29,9 @@ const defaults = {
use: ['@svgr/webpack'],
}
]
+ },
+ resolve: {
+ extensions: ['.js', '.ts', '.tsx']
}
};
diff --git a/dash/dash.py b/dash/dash.py
index 577ad51607..75c7ba6436 100644
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -1605,8 +1605,12 @@ def verify_url_part(served_part, url_part, part_name):
display_url = (protocol, host, ":{}".format(port), path)
self.logger.info("Dash is running on %s://%s%s%s\n", *display_url)
- self.logger.info(" Warning: This is a development server. Do not use app.run_server")
- self.logger.info(" in production, use a production WSGI server like gunicorn instead.\n")
+ self.logger.info(
+ " Warning: This is a development server. Do not use app.run_server"
+ )
+ self.logger.info(
+ " in production, use a production WSGI server like gunicorn instead.\n"
+ )
if not os.environ.get("FLASK_ENV"):
os.environ["FLASK_ENV"] = "development"
diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py
index 63b30d407a..caedb9b5e7 100644
--- a/dash/testing/dash_page.py
+++ b/dash/testing/dash_page.py
@@ -36,13 +36,33 @@ def redux_state_paths(self):
def redux_state_rqs(self):
return self.driver.execute_script(
"""
- return window.store.getState().pendingCallbacks.map(function(cb) {
- var out = {};
- for (var key in cb) {
- if (typeof cb[key] !== 'function') { out[key] = cb[key]; }
- }
- return out;
- })
+
+ // Check for legacy `pendingCallbacks` store prop (compatibility for Dash matrix testing)
+ var pendingCallbacks = window.store.getState().pendingCallbacks;
+ if (pendingCallbacks) {
+ return pendingCallbacks.map(function(cb) {
+ var out = {};
+ for (var key in cb) {
+ if (typeof cb[key] !== 'function') { out[key] = cb[key]; }
+ }
+ return out;
+ });
+ }
+
+ // Otherwise, use the new `callbacks` store prop
+ var callbacksState = Object.assign({}, window.store.getState().callbacks);
+ delete callbacksState.stored;
+ delete callbacksState.completed;
+
+ return Array.prototype.concat.apply([], Object.values(callbacksState));
+ """
+ )
+
+ @property
+ def redux_state_is_loading(self):
+ return self.driver.execute_script(
+ """
+ return window.store.getState().isLoading;
"""
)
@@ -51,7 +71,7 @@ def window_store(self):
return self.driver.execute_script("return window.store")
def _wait_for_callbacks(self):
- return not self.window_store or self.redux_state_rqs == []
+ return not self.window_store or self.redux_state_rqs
def get_local_storage(self, store_id="local"):
return self.driver.execute_script(
diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py
index 3d84d7a07f..f7f37c2f47 100644
--- a/tests/integration/callbacks/test_basic_callback.py
+++ b/tests/integration/callbacks/test_basic_callback.py
@@ -41,7 +41,7 @@ def update_output(value):
assert call_count.value == 2 + len("hello world"), "initial count + each key stroke"
- assert dash_duo.redux_state_rqs == []
+ assert not dash_duo.redux_state_is_loading
assert dash_duo.get_logs() == []
@@ -133,7 +133,7 @@ def update_input(value):
"#sub-output-1", pad_input.attrs["value"] + "deadbeef"
)
- assert dash_duo.redux_state_rqs == [], "pendingCallbacks is empty"
+ assert not dash_duo.redux_state_is_loading, "loadingMap is empty"
dash_duo.percy_snapshot(name="callback-generating-function-2")
assert dash_duo.get_logs() == [], "console is clean"
diff --git a/tests/integration/callbacks/test_callback_context.py b/tests/integration/callbacks/test_callback_context.py
index bddca9c6de..f4f4552c4c 100644
--- a/tests/integration/callbacks/test_callback_context.py
+++ b/tests/integration/callbacks/test_callback_context.py
@@ -17,7 +17,7 @@ def test_cbcx001_modified_response(dash_duo):
@app.callback(Output("output", "children"), [Input("input", "value")])
def update_output(value):
- callback_context.response.set_cookie("dash cookie", value + " - cookie")
+ callback_context.response.set_cookie("dash_cookie", value + " - cookie")
return value + " - output"
dash_duo.start_server(app)
@@ -27,7 +27,7 @@ def update_output(value):
input1.send_keys("cd")
dash_duo.wait_for_text_to_equal("#output", "abcd - output")
- cookie = dash_duo.driver.get_cookie("dash cookie")
+ cookie = dash_duo.driver.get_cookie("dash_cookie")
# cookie gets json encoded
assert cookie["value"] == '"abcd - cookie"'
diff --git a/tests/integration/callbacks/test_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py
index 80656d5b5f..163aa01e7e 100644
--- a/tests/integration/callbacks/test_layout_paths_with_callbacks.py
+++ b/tests/integration/callbacks/test_layout_paths_with_callbacks.py
@@ -176,7 +176,7 @@ def check_chapter(chapter):
TIMEOUT,
)
- assert dash_duo.redux_state_rqs == [], "pendingCallbacks is empty"
+ assert not dash_duo.redux_state_is_loading, "loadingMap is empty"
def check_call_counts(chapters, count):
for chapter in chapters:
diff --git a/tests/integration/callbacks/test_missing_inputs.py b/tests/integration/callbacks/test_missing_inputs.py
index 2cfffb8ecc..8ee1df8bb2 100644
--- a/tests/integration/callbacks/test_missing_inputs.py
+++ b/tests/integration/callbacks/test_missing_inputs.py
@@ -9,7 +9,7 @@ def wait_for_queue(dash_duo):
# mostly for cases where no callbacks should fire:
# just wait until we have the button and the queue is empty
dash_duo.wait_for_text_to_equal("#btn", "click")
- wait.until(lambda: dash_duo.redux_state_rqs == [], 3)
+ wait.until(lambda: not dash_duo.redux_state_is_loading, 3)
def test_cbmi001_all_missing_inputs(dash_duo):
diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py
index d9b13503a0..8c081d2cad 100644
--- a/tests/integration/callbacks/test_multiple_callbacks.py
+++ b/tests/integration/callbacks/test_multiple_callbacks.py
@@ -32,7 +32,7 @@ def update_output(n_clicks):
assert call_count.value == 4, "get called 4 times"
assert dash_duo.find_element("#output").text == "3", "clicked button 3 times"
- assert dash_duo.redux_state_rqs == []
+ assert not dash_duo.redux_state_is_loading
dash_duo.percy_snapshot(
name="test_callbacks_called_multiple_times_and_out_of_order"
diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py
index 2dbfeb368a..2cd14954a2 100644
--- a/tests/integration/callbacks/test_wildcards.py
+++ b/tests/integration/callbacks/test_wildcards.py
@@ -6,6 +6,7 @@
import dash_html_components as html
import dash_core_components as dcc
import dash
+from dash.testing import wait
from dash.dependencies import Input, Output, State, ALL, ALLSMALLER, MATCH
@@ -229,7 +230,17 @@ def assert_item(item, text, done, prefix="", suffix=""):
assert_count(0)
+fibonacci_count = 0
+fibonacci_sum_count = 0
+
+
def fibonacci_app(clientside):
+ global fibonacci_count
+ global fibonacci_sum_count
+
+ fibonacci_count = 0
+ fibonacci_sum_count = 0
+
# This app tests 2 things in particular:
# - clientside callbacks work the same as server-side
# - callbacks using ALLSMALLER as an input to MATCH of the exact same id/prop
@@ -275,12 +286,20 @@ def items(n):
Output({"i": MATCH}, "children"), [Input({"i": ALLSMALLER}, "children")]
)
def sequence(prev):
+ global fibonacci_count
+ fibonacci_count = fibonacci_count + 1
+ print(fibonacci_count)
+
if len(prev) < 2:
return len(prev)
return int(prev[-1] or 0) + int(prev[-2] or 0)
@app.callback(Output("sum", "children"), [Input({"i": ALL}, "children")])
def show_sum(seq):
+ global fibonacci_sum_count
+ fibonacci_sum_count = fibonacci_sum_count + 1
+ print("fibonacci_sum_count: ", fibonacci_sum_count)
+
return "{} elements, sum: {}".format(
len(seq), sum(int(v or 0) for v in seq)
)
@@ -454,3 +473,46 @@ def update_output_on_page_pattern(value):
trigger_text = 'triggered is Truthy with prop_ids {"index":1,"type":"input"}.value'
dash_duo.wait_for_text_to_equal("#output-outer", trigger_text)
dash_duo.wait_for_text_to_equal("#output-inner", trigger_text)
+
+
+def test_cbwc005_callbacks_count(dash_duo):
+ global fibonacci_count
+ global fibonacci_sum_count
+
+ app = fibonacci_app(False)
+ dash_duo.start_server(app)
+
+ wait.until(lambda: fibonacci_count == 4, 3) # initial
+ wait.until(lambda: fibonacci_sum_count == 2, 3) # initial + triggered
+
+ dash_duo.find_element("#n").send_keys(Keys.UP) # 5
+ wait.until(lambda: fibonacci_count == 9, 3)
+ wait.until(lambda: fibonacci_sum_count == 3, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.UP) # 6
+ wait.until(lambda: fibonacci_count == 15, 3)
+ wait.until(lambda: fibonacci_sum_count == 4, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 5
+ wait.until(lambda: fibonacci_count == 20, 3)
+ wait.until(lambda: fibonacci_sum_count == 5, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 4
+ wait.until(lambda: fibonacci_count == 24, 3)
+ wait.until(lambda: fibonacci_sum_count == 6, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 3
+ wait.until(lambda: fibonacci_count == 27, 3)
+ wait.until(lambda: fibonacci_sum_count == 7, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 2
+ wait.until(lambda: fibonacci_count == 29, 3)
+ wait.until(lambda: fibonacci_sum_count == 8, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 1
+ wait.until(lambda: fibonacci_count == 30, 3)
+ wait.until(lambda: fibonacci_sum_count == 9, 3)
+
+ dash_duo.find_element("#n").send_keys(Keys.DOWN) # 0
+ wait.until(lambda: fibonacci_count == 30, 3)
+ wait.until(lambda: fibonacci_sum_count == 10, 3)
diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py
index 6213d71f7b..b29413d1d6 100644
--- a/tests/integration/renderer/test_dependencies.py
+++ b/tests/integration/renderer/test_dependencies.py
@@ -40,6 +40,6 @@ def update_output_2(value):
assert output_1_call_count.value == 2 and output_2_call_count.value == 0
- assert dash_duo.redux_state_rqs == []
+ assert not dash_duo.redux_state_is_loading
assert dash_duo.get_logs() == []
diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py
index 3ec1ce40ba..e46314bc51 100644
--- a/tests/integration/renderer/test_due_diligence.py
+++ b/tests/integration/renderer/test_due_diligence.py
@@ -95,7 +95,7 @@ def test_rddd001_initial_state(dash_duo):
)
}, "paths should reflect to the component hierarchy"
- assert dash_duo.redux_state_rqs == [], "no callback => no pendingCallbacks"
+ assert not dash_duo.redux_state_is_loading, "no callback => no pendingCallbacks"
dash_duo.percy_snapshot(name="layout")
assert dash_duo.get_logs() == [], "console has no errors"
diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py
index ed3bc7a180..1a79090323 100644
--- a/tests/integration/test_render.py
+++ b/tests/integration/test_render.py
@@ -490,10 +490,13 @@ def update_output(n_clicks):
self.assertEqual(call_count.value, 3)
self.wait_for_text_to_equal("#output1", "2")
self.wait_for_text_to_equal("#output2", "3")
- pending_count = self.driver.execute_script(
- "return window.store.getState().pendingCallbacks.length"
+ ready = self.driver.execute_script(
+ """
+ return !window.store.getState().isLoading;
+ """
)
- self.assertEqual(pending_count, 0)
+
+ assert ready
def test_callbacks_with_shared_grandparent(self):
app = Dash()