From a4c699eabd2554fa0a88bf2ed5db4ec77754f12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 11:16:53 -0400 Subject: [PATCH 01/90] add typescript to toolchain --- dash-renderer/@Types/modules.d.ts | 9 ++ dash-renderer/package-lock.json | 138 ++++++++++++++++++++++++++++++ dash-renderer/package.json | 7 ++ dash-renderer/tsconfig.json | 22 +++++ dash-renderer/webpack.config.js | 8 ++ 5 files changed, 184 insertions(+) create mode 100644 dash-renderer/@Types/modules.d.ts create mode 100644 dash-renderer/tsconfig.json 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/package-lock.json b/dash-renderer/package-lock.json index 1c452c4a1d..47af7a2c44 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -3246,6 +3246,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 +3311,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.26.21", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.26.21.tgz", + "integrity": "sha512-zMYtIZMceA6BvH+or6LmewLBgojbXg5+FGCwjO8K+Z+d/ZWxILmhhASXkehW0PqJL+V0QbyDeeAHix0dvEKXfQ==", + "dev": true, + "requires": { + "ts-toolbelt": "^3.8.4" + } + }, + "@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", @@ -5599,6 +5661,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", @@ -18315,6 +18383,76 @@ "integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==", "dev": true }, + "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": "3.14.0", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.14.0.tgz", + "integrity": "sha512-DYjD8tL7M1kBogRd9UKg3bUP5yh69WWcMSaA3By8ATiJU9fgYudSYIe8tWD5cpPkrGCdGGnKXQHG+5IrjJ5uhQ==", + "dev": true + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index 3b383ef7f6..f0c8842805 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -45,6 +45,11 @@ "@babel/preset-env": "^7.8.7", "@babel/preset-react": "^7.8.3", "@svgr/webpack": "^5.2.0", + "@types/ramda": "^0.26.21", + "@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 +68,8 @@ "prettier-eslint-cli": "^5.0.0", "prettier-stylelint": "^0.4.2", "style-loader": "^1.1.3", + "ts-loader": "^7.0.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/tsconfig.json b/dash-renderer/tsconfig.json new file mode 100644 index 0000000000..00915963c6 --- /dev/null +++ b/dash-renderer/tsconfig.json @@ -0,0 +1,22 @@ +{ + "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 + } +} \ No newline at end of file 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'] } }; From 93ef3921db3949d7c868fd431f78f9fc60eb0bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 11:42:37 -0400 Subject: [PATCH 02/90] Add StoreObserver --- dash-renderer/src/StoreObserver.ts | 121 +++++++++++++++++++++++ dash-renderer/src/{store.js => store.ts} | 30 ++++-- 2 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 dash-renderer/src/StoreObserver.ts rename dash-renderer/src/{store.js => store.ts} (55%) diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts new file mode 100644 index 0000000000..f952a00f4b --- /dev/null +++ b/dash-renderer/src/StoreObserver.ts @@ -0,0 +1,121 @@ +import { + any, + assocPath, + concat, + forEach, + map, + path, + reduce +} from 'ramda'; + +import { Store, Unsubscribe } from 'redux'; + +type Observer = (store: Store) => void; +type UnregisterObserver = () => void; + +interface IStoreObserver { + inputPaths: string[][]; + lastState: any; + observer: Observer; + statePaths: string[][]; + triggered: boolean; +} + +export default class StoreObserver { + private _store?: Store; + private _unsubscribe?: Unsubscribe; + + private readonly _observers: IStoreObserver[] = []; + + constructor(store?: Store) { + this.__init__(store); + } + + observe = ( + observer: Observer, + inputs: string[], + states: string[] = [] + ): UnregisterObserver => { + this.add(observer, inputs, states); + + return () => this.remove(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[], + states: string[] + ) => this._observers.push({ + inputPaths: map(p => p.split('.'), inputs), + lastState: null, + observer, + statePaths: map(p => p.split('.'), states), + triggered: false + }); + + private notify = () => forEach( + this.notifyObserver, + this._observers + ); + + private notifyObserver = (o: IStoreObserver) => { + const store = this._store; + if (!store) { + return; + } + + const state: any = store.getState(); + + /** Don't trigger if nested */ + if (o.triggered) { + return; + } + + const { inputPaths, lastState, observer, statePaths } = o; + + /** Don't notify observer if there's no change */ + if (!any( + i => path(i, state) !== path(i, lastState), + inputPaths + )) { + return; + } + + o.triggered = true; + observer({ + ...store, + /** Build partial state that interests the observer */ + getState: () => reduce((s, p) => assocPath( + p, + path(p, state), + s + ), {}, concat(inputPaths, statePaths)) + }); + + o.triggered = false; + o.lastState = store.getState(); + }; + + private remove = (observer: Observer) => this._observers.splice( + this._observers.findIndex( + o => observer === o.observer, + this._observers + ), 1 + ); +} \ No newline at end of file diff --git a/dash-renderer/src/store.js b/dash-renderer/src/store.ts similarity index 55% rename from dash-renderer/src/store.js rename to dash-renderer/src/store.ts index fc9fa7f465..e27baa370e 100644 --- a/dash-renderer/src/store.js +++ b/dash-renderer/src/store.ts @@ -1,8 +1,18 @@ -import {createStore, applyMiddleware} from 'redux'; +import { createStore, applyMiddleware, Store } from 'redux'; import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; +import StoreObserver from './StoreObserver'; -let store; + +let store: Store; +const storeObserver = new StoreObserver(); + +export const observe = storeObserver.observe; + +function createAppStore(reducer: any, middleware: any) { + store = createStore(reducer, middleware); + storeObserver.setStore(store); +} /** * Initialize a Redux store with thunk, plus logging (only in development mode) middleware @@ -12,7 +22,7 @@ let store; * @returns {Store} * An initialized redux store with middleware and possible hot reloading of reducers */ -const initializeStore = reset => { +const initializeStore = (reset?: boolean): Store => { if (store && !reset) { return store; } @@ -21,25 +31,25 @@ const initializeStore = reset => { // eslint-disable-next-line no-process-env if (process.env.NODE_ENV === 'production') { - store = createStore(reducer, applyMiddleware(thunk)); + createAppStore(reducer, applyMiddleware(thunk)); } else { // only attach logger to middleware in non-production mode - const reduxDTEC = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; + const reduxDTEC = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; if (reduxDTEC) { - store = createStore(reducer, reduxDTEC(applyMiddleware(thunk))); + createAppStore(reducer, reduxDTEC(applyMiddleware(thunk))); } else { - store = createStore(reducer, applyMiddleware(thunk)); + createAppStore(reducer, applyMiddleware(thunk)); } } if (!reset) { // TODO - Protect this under a debug mode? - window.store = store; + (window as any).store = store; } - if (module.hot) { + if ((module as any).hot) { // Enable hot module replacement for reducers - module.hot.accept('./reducers/reducer', () => { + (module as any).hot.accept('./reducers/reducer', () => { const nextRootReducer = require('./reducers/reducer').createReducer(); store.replaceReducer(nextRootReducer); From 6955746be6cb56b8cf6507df8c0029f9bd496897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 11:44:55 -0400 Subject: [PATCH 03/90] Add callbacks reducer --- dash-renderer/src/reducers/callbacks.ts | 100 ++++++++++++++++++++++++ dash-renderer/src/reducers/reducer.js | 26 +++--- 2 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 dash-renderer/src/reducers/callbacks.ts diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts new file mode 100644 index 0000000000..5dacb94d8f --- /dev/null +++ b/dash-renderer/src/reducers/callbacks.ts @@ -0,0 +1,100 @@ +import { + concat, + difference, + reduce +} from 'ramda'; + +/** + * Callback states and transitions + * + * State transition --> {State} + * ------------------------------- + * {Requested} prioritize --> {Prioritized} + * {Prioritized} execute --> {Executing} + * {Executing} processResult --> {Executed} + * {Executed} (none) + */ + +export enum CallbackActionType { + AddExecuted = 'Callbacks.AddExecuted', + AddExecuting = 'Callbacks.AddExecuting', + AddRequested = 'Callbacks.AddRequested', + AddPrioritized = 'Callbacks.AddPrioritized', + RemoveExecuted = 'Callbacks.RemoveExecuted', + RemoveExecuting = 'Callbacks.RemoveExecuting', + RemoveRequested = 'Callbacks.RemoveRequested', + RemovePrioritized = 'Callbacks.ReomvePrioritized', +} + +export enum CallbackAggregateActionType { + Aggregate = 'Callbacks.Aggregate' +} + +export type Callback = any; + +interface ICallbackAction { + type: CallbackActionType | CallbackAggregateActionType | string; + payload: Callback[]; +} + +type CallbackAction = ICallbackAction | { + type: CallbackAggregateActionType.Aggregate, + payload: ICallbackAction[] +}; + + +export interface ICallbacksState { + executed: Callback[]; + executing: Callback[]; + prioritized: Callback[]; + requested: Callback[]; +} + +const DEFAULT_STATE = { + executed: [], + executing: [], + prioritized: [], + requested: [] +}; + +const transforms: { + [key: string]: (a1: Callback[], a2: Callback[]) => Callback[] +} = { + [CallbackActionType.AddExecuted]: concat, + [CallbackActionType.AddExecuting]: concat, + [CallbackActionType.AddPrioritized]: concat, + [CallbackActionType.AddRequested]: concat, + [CallbackActionType.RemoveExecuted]: difference, + [CallbackActionType.RemoveExecuting]: difference, + [CallbackActionType.RemovePrioritized]: difference, + [CallbackActionType.RemoveRequested]: difference, +}; + +const fields: { + [key: string]: keyof ICallbacksState +} = { + [CallbackActionType.AddExecuted]: 'executed', + [CallbackActionType.AddExecuting]: 'executing', + [CallbackActionType.AddPrioritized]: 'prioritized', + [CallbackActionType.AddRequested]: 'requested', + [CallbackActionType.RemoveExecuted]: 'executed', + [CallbackActionType.RemoveExecuting]: 'executing', + [CallbackActionType.RemovePrioritized]: 'prioritized', + [CallbackActionType.RemoveRequested]: 'requested', +} + +export default ( + state: ICallbacksState = DEFAULT_STATE, + action: CallbackAction +) => reduce((s, a) => { + const transform = transforms[a.type]; + const field = fields[a.type]; + + return (!transform || !field || a.payload.length === 0) ? s : { + ...s, + [field]: transform(s[field], a.payload) + }; +}, state, action.type === CallbackAggregateActionType.Aggregate ? + action.payload : + [action] +); \ No newline at end of file diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index ffdb8794fa..58760cc879 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -3,16 +3,17 @@ import {combineReducers} from 'redux'; import {getCallbacksByInput} from '../actions/dependencies'; -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 layout from './layout'; +import paths from './paths'; +import pendingCallbacks from './pendingCallbacks'; export const apiRequests = [ 'dependenciesRequest', @@ -24,14 +25,15 @@ export const apiRequests = [ function mainReducer() { const parts = { appLifecycle, - layout, - graphs, - paths, - pendingCallbacks, + callbacks, config, - history, error, + graphs, + history, hooks, + layout, + paths, + pendingCallbacks, }; forEach(r => { parts[r] = createApiReducer(r); From 8eff2a85ef9ee8062fb6d67f3dec74f08e880870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 12:13:33 -0400 Subject: [PATCH 04/90] Improve Store and StoreObserver typing --- dash-renderer/src/StoreObserver.ts | 28 ++++++++++++++-------------- dash-renderer/src/store.ts | 13 ++++++++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index f952a00f4b..a5a6f016ab 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -10,29 +10,29 @@ import { import { Store, Unsubscribe } from 'redux'; -type Observer = (store: Store) => void; +type Observer = (store: TStore) => void; type UnregisterObserver = () => void; -interface IStoreObserver { +interface IStoreObserver { inputPaths: string[][]; lastState: any; - observer: Observer; + observer: Observer; statePaths: string[][]; triggered: boolean; } -export default class StoreObserver { - private _store?: Store; +export default class StoreObserver { + private _store?: TStore; private _unsubscribe?: Unsubscribe; - private readonly _observers: IStoreObserver[] = []; + private readonly _observers: IStoreObserver[] = []; - constructor(store?: Store) { + constructor(store?: TStore) { this.__init__(store); } observe = ( - observer: Observer, + observer: Observer, inputs: string[], states: string[] = [] ): UnregisterObserver => { @@ -41,14 +41,14 @@ export default class StoreObserver { return () => this.remove(observer); } - setStore = (store: Store) => { + setStore = (store: TStore) => { this.__finalize__(); this.__init__(store); } private __finalize__ = () => this._unsubscribe?.() - private __init__ = (store?: Store) => { + private __init__ = (store?: TStore) => { this._store = store; if (store) { this._unsubscribe = store.subscribe(this.notify); @@ -58,7 +58,7 @@ export default class StoreObserver { } private add = ( - observer: Observer, + observer: Observer, inputs: string[], states: string[] ) => this._observers.push({ @@ -74,7 +74,7 @@ export default class StoreObserver { this._observers ); - private notifyObserver = (o: IStoreObserver) => { + private notifyObserver = (o: IStoreObserver) => { const store = this._store; if (!store) { return; @@ -105,14 +105,14 @@ export default class StoreObserver { p, path(p, state), s - ), {}, concat(inputPaths, statePaths)) + ), {}, concat(inputPaths, statePaths)) as TStore }); o.triggered = false; o.lastState = store.getState(); }; - private remove = (observer: Observer) => this._observers.splice( + private remove = (observer: Observer) => this._observers.splice( this._observers.findIndex( o => observer === o.observer, this._observers diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index e27baa370e..72311180ee 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -2,10 +2,17 @@ import { createStore, applyMiddleware, Store } from 'redux'; import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; +import { ICallbacksState } from './reducers/callbacks'; +interface IStoreState { + callbacks: ICallbacksState; + [key: string]: any; +} + +type DashStore = Store; -let store: Store; -const storeObserver = new StoreObserver(); +let store: DashStore; +const storeObserver = new StoreObserver(); export const observe = storeObserver.observe; @@ -22,7 +29,7 @@ function createAppStore(reducer: any, middleware: any) { * @returns {Store} * An initialized redux store with middleware and possible hot reloading of reducers */ -const initializeStore = (reset?: boolean): Store => { +const initializeStore = (reset?: boolean): DashStore => { if (store && !reset) { return store; } From 8d7b045a16b52e12d461f4d09ea5d741b3672735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 12:26:56 -0400 Subject: [PATCH 05/90] Add callback actions --- dash-renderer/src/actions/index.js | 44 ++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 83972f4bcc..4da204853e 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -18,6 +18,10 @@ import { zip, } from 'ramda'; import {createAction} from 'redux-actions'; +import { + CallbackActionType, + CallbackAggregateActionType, +} from '../reducers/callbacks'; import {getAppState} from '../reducers/constants'; import {getAction} from './constants'; import cookie from 'cookie'; @@ -44,16 +48,44 @@ import {applyPersistence, prunePersistence} from '../persistence'; 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 setPendingCallbacks = createAction('SET_PENDING_CALLBACKS'); +export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); +export const updateProps = createAction(getAction('ON_PROP_CHANGE')); + +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 removeExecutedCallbacks = createAction( + CallbackActionType.RemoveExecuted +); +export const removeExecutingCallbacks = createAction( + CallbackActionType.RemoveExecuting +); +export const removePrioritizedCallbacks = createAction( + CallbackActionType.RemovePrioritized +); +export const removeRequestedCallbacks = createAction( + CallbackActionType.RemoveRequested +); +export const aggregateCallbacks = createAction( + CallbackAggregateActionType.Aggregate +); export const dispatchError = dispatch => (message, lines) => dispatch( From 0decbbaa3e87b469939722566cdbb873048a9ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 15:49:49 -0400 Subject: [PATCH 06/90] Only consider TS code inside /src --- dash-renderer/tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dash-renderer/tsconfig.json b/dash-renderer/tsconfig.json index 00915963c6..ca13e15e1c 100644 --- a/dash-renderer/tsconfig.json +++ b/dash-renderer/tsconfig.json @@ -18,5 +18,8 @@ "strictPropertyInitialization": true, "target": "esnext", "traceResolution": false - } + }, + "include": [ + "src/*" + ] } \ No newline at end of file From e2d9ce8a800c896886348cb69474848af0086d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 7 May 2020 15:51:15 -0400 Subject: [PATCH 07/90] Add `Watched` state to the callbacks state machine --- dash-renderer/src/reducers/callbacks.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 5dacb94d8f..d7cdf728c8 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -18,19 +18,28 @@ import { export enum CallbackActionType { AddExecuted = 'Callbacks.AddExecuted', AddExecuting = 'Callbacks.AddExecuting', - AddRequested = 'Callbacks.AddRequested', AddPrioritized = 'Callbacks.AddPrioritized', + AddRequested = 'Callbacks.AddRequested', + AddWatched = 'Callbacks.Watched', RemoveExecuted = 'Callbacks.RemoveExecuted', RemoveExecuting = 'Callbacks.RemoveExecuting', - RemoveRequested = 'Callbacks.RemoveRequested', RemovePrioritized = 'Callbacks.ReomvePrioritized', + RemoveRequested = 'Callbacks.RemoveRequested', + RemoveWatched = 'Callbacks.RemoveWatched' } export enum CallbackAggregateActionType { Aggregate = 'Callbacks.Aggregate' } -export type Callback = any; +export type CallbackResult = { + data: any; +} | { error: any }; + +export type Callback = { + executionResult?: Promise | CallbackResult | null; + [key: string]: any; +}; interface ICallbackAction { type: CallbackActionType | CallbackAggregateActionType | string; @@ -48,13 +57,15 @@ export interface ICallbacksState { executing: Callback[]; prioritized: Callback[]; requested: Callback[]; + watched: Callback[]; } -const DEFAULT_STATE = { +const DEFAULT_STATE: ICallbacksState = { executed: [], executing: [], prioritized: [], - requested: [] + requested: [], + watched: [] }; const transforms: { @@ -64,10 +75,12 @@ const transforms: { [CallbackActionType.AddExecuting]: concat, [CallbackActionType.AddPrioritized]: concat, [CallbackActionType.AddRequested]: concat, + [CallbackActionType.AddWatched]: concat, [CallbackActionType.RemoveExecuted]: difference, [CallbackActionType.RemoveExecuting]: difference, [CallbackActionType.RemovePrioritized]: difference, [CallbackActionType.RemoveRequested]: difference, + [CallbackActionType.RemoveWatched]: difference, }; const fields: { @@ -77,10 +90,12 @@ const fields: { [CallbackActionType.AddExecuting]: 'executing', [CallbackActionType.AddPrioritized]: 'prioritized', [CallbackActionType.AddRequested]: 'requested', + [CallbackActionType.AddWatched]: 'watched', [CallbackActionType.RemoveExecuted]: 'executed', [CallbackActionType.RemoveExecuting]: 'executing', [CallbackActionType.RemovePrioritized]: 'prioritized', [CallbackActionType.RemoveRequested]: 'requested', + [CallbackActionType.RemoveWatched]: 'watched' } export default ( From 5c5bf592a1badb95076a386e262935497916ccda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 12 May 2020 18:02:54 -0400 Subject: [PATCH 08/90] handle duplicates, pruning, errors --- dash-renderer/src/AppProvider.react.js | 33 - dash-renderer/src/AppProvider.react.tsx | 487 ++++++++++++++ dash-renderer/src/StoreObserver.ts | 34 +- dash-renderer/src/TreeContainer.js | 4 +- dash-renderer/src/actions/callbacks.ts | 359 +++++++++++ dash-renderer/src/actions/dependencies.js | 173 +---- dash-renderer/src/actions/dependencies_ts.ts | 50 ++ dash-renderer/src/actions/index.js | 628 +------------------ dash-renderer/src/reducers/callbacks.ts | 123 ++-- dash-renderer/src/store.ts | 3 +- 10 files changed, 1008 insertions(+), 886 deletions(-) delete mode 100644 dash-renderer/src/AppProvider.react.js create mode 100644 dash-renderer/src/AppProvider.react.tsx create mode 100644 dash-renderer/src/actions/callbacks.ts create mode 100644 dash-renderer/src/actions/dependencies_ts.ts diff --git a/dash-renderer/src/AppProvider.react.js b/dash-renderer/src/AppProvider.react.js deleted file mode 100644 index d44a27eb61..0000000000 --- a/dash-renderer/src/AppProvider.react.js +++ /dev/null @@ -1,33 +0,0 @@ -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}) => { - return ( - - - - ); -}; - -AppProvider.propTypes = { - hooks: PropTypes.shape({ - request_pre: PropTypes.func, - request_post: PropTypes.func, - }), -}; - -AppProvider.defaultProps = { - hooks: { - request_pre: null, - request_post: null, - }, -}; - -export default AppProvider; diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx new file mode 100644 index 0000000000..8edc624e35 --- /dev/null +++ b/dash-renderer/src/AppProvider.react.tsx @@ -0,0 +1,487 @@ +import { + all, + assoc, + concat, + difference, + filter, + find, + flatten, + forEach, + groupBy, + has, + isEmpty, + isNil, + map, + partition, + path, + pick, + pickBy, + pluck, + reduce, + uniq, + values +} from 'ramda'; + +import React from 'react'; +import {Provider} from 'react-redux'; + +import initializeStore, { observe } from './store'; +import AppContainer from './AppContainer.react'; + +import PropTypes from 'prop-types'; +import { + updateProps, + setPaths, + handleAsyncError +} from './actions'; +import { + addExecutedCallbacks, + addExecutingCallbacks, + addPrioritizedCallbacks, + aggregateCallbacks, + removePrioritizedCallbacks, + removeRequestedCallbacks, + setPendingCallbacks, + addWatchedCallbacks, + removeExecutingCallbacks, + removeWatchedCallbacks, + removeExecutedCallbacks, + addCompletedCallbacks, + addRequestedCallbacks +} from './actions/callbacks'; +import { getPath, computePaths } from './actions/paths'; + +import { + executeCallback +} from './actions/callbacks'; +import { stringifyId, combineIdAndProp, parseIfWildcard, getCallbacksInLayout } from './actions/dependencies'; +import { includeObservers, pruneCallbacks } from './actions/dependencies_ts'; +import { ICallbacksState, IExecutingCallback, ICallback } from './reducers/callbacks'; +import isAppReady from './actions/isAppReady'; +import { prunePersistence, applyPersistence } from './persistence'; + +const store = initializeStore(); + +const getPendingCallbacks = ({ executed, executing, prioritized, requested, watched }: ICallbacksState) => [ + ...requested, + ...prioritized, + ...executing, + ...watched, + ...executed +]; + +observe(({ + dispatch, + getState +}) => { + const { + callbacks, + pendingCallbacks + } = getState(); + + const next = getPendingCallbacks(callbacks); + console.log('onCallbacksChanged', '[pendingCallbacks-candidate]', next); + + /** + * If the calculated list of pending callbacks is equivalent + * to the previous one, do not update it. + */ + if ( + pendingCallbacks && + pendingCallbacks.length === next.length && + next.every((v, i) => + v === pendingCallbacks[i] || + v.callback === pendingCallbacks[i].callback) + ) { + return; + } + + console.log('onCallbacksChanged', '[pendingCallbacks]', next); + dispatch(setPendingCallbacks(next)); +}, ['callbacks']); + +observe(({ + dispatch, + getState +}) => { + const { callbacks, callbacks: { prioritized, executing, watched, completed }, paths } = getState(); + let { callbacks: { requested } } = getState(); + + const pendingCallbacks = getPendingCallbacks(callbacks); + + console.log('onCallbacksChanged.requested', completed, requested); + + /* + 1. Remove duplicated `requested` callbacks + */ + + + /* + Extract all but the first callback from each IOS-key group + these callbacks are duplicates. + */ + const rDuplicates = flatten(map( + group => group.slice(1), + values( + groupBy( + r => JSON.stringify( + pick(['inputs', 'outputs', 'state'], r.callback) + ), + requested + ) + ) + )); + + /* + 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(1), + values( + groupBy( + r => JSON.stringify( + pick(['inputs', 'outputs', 'state'], r.callback) + ), + concat(requested, prioritized) + ) + ) + )); + + const eDuplicates = flatten(map( + group => group.slice(1), + values( + groupBy( + r => JSON.stringify( + pick(['inputs', 'outputs', 'state'], r.callback) + ), + concat(requested, executing) + ) + ) + )) as IExecutingCallback[]; + + const wDuplicates = flatten(map( + group => group.slice(1), + values( + groupBy( + r => JSON.stringify( + pick(['inputs', 'outputs', 'state'], r.callback) + ), + concat(requested, watched) + ) + ) + )) as IExecutingCallback[]; + + if (rDuplicates.length || pDuplicates.length || eDuplicates.length || wDuplicates.length) { + console.log('onCallbacksChanged.requested', '[duplicates]', rDuplicates.length, pDuplicates.length, eDuplicates.length, wDuplicates.length); + } + + /* + 3. Modify or remove callbacks that are outputing to non-existing layout `id`. + */ + + const { pruned: rPruned, initial: rInitial, modified: rModified, removed: rRemoved } = pruneCallbacks(requested, paths); + const { pruned: pPruned, initial: pInitial, modified: pModified, removed: pRemoved } = pruneCallbacks(prioritized, paths); + const { pruned: ePruned, initial: eInitial, modified: eModified, removed: eRemoved } = pruneCallbacks(executing, paths); + const { pruned: wPruned, initial: wInitial, modified: wModified, removed: wRemoved } = pruneCallbacks(watched, paths); + + if (rPruned + pPruned + ePruned + wPruned) { + console.log('onCallbacksChanged.requested', '[pruned]', rPruned, pPruned, ePruned, wPruned); + } + + /* + Clean up the `requested` list - during the dispatch phase, + it will be updated for real + */ + requested = concat( + difference( + requested, + concat(rInitial, rRemoved) + ), + rModified + ); + + /* 4. Determine `requested` callbacks that can be `prioritized` */ + /* Find all outputs of all active callbacks */ + const outputs = map( + o => `${o.id}.${o.property}`, + reduce((o, cb) => concat(o, cb.callback.outputs), [], pendingCallbacks) + ); + + /* 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) */ + const readyCallbacks = filter( + cb => all( + i => !outputsMap[`${i.id}.${i.property}`], + cb.callback.inputs + ), + requested + ); + + dispatch(aggregateCallbacks([ + // Clean up duplicated callbacks + rDuplicates.length ? removeRequestedCallbacks(rDuplicates) : null, + pDuplicates.length ? removePrioritizedCallbacks(pDuplicates) : null, + eDuplicates.length ? removeExecutingCallbacks(eDuplicates) : null, + wDuplicates.length ? removeWatchedCallbacks(wDuplicates) : null, + // Prune callbacks + (rInitial.length + rRemoved.length) ? removeRequestedCallbacks(concat(rInitial, rRemoved)) : null, + rModified.length ? addRequestedCallbacks(rModified) : null, + (pInitial.length + pRemoved.length) ? removePrioritizedCallbacks(concat(pInitial, pRemoved)) : null, + pModified.length ? addPrioritizedCallbacks(pModified) : null, + (eInitial.length + eRemoved.length) ? removeExecutingCallbacks(concat(eInitial, eRemoved)) : null, + eModified.length ? addExecutingCallbacks(eModified) : null, + (wInitial.length + wRemoved.length) ? removeWatchedCallbacks(concat(wInitial, wRemoved)) : null, + wModified.length ? addWatchedCallbacks(wModified) : null, + // Promoted callbacks + readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, + readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null + ])); +}, ['callbacks.requested', 'callbacks.completed']); + +observe(async ({ + dispatch, + getState +}) => { + const { callbacks: { executing, watched }, config, hooks, layout, paths } = getState(); + let { callbacks: { prioritized } } = getState(); + + console.log('onCallbacksChanged.prioritized', prioritized); + + const available = Math.max( + 0, + 6 - executing.length - watched.length + ); + + prioritized = prioritized.slice(0, available); + if (!prioritized.length) { + return; + } + + const callbacks: [ICallback, any][] = prioritized.map(cb => { + 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 })); + }); + cb.requestedOutputs = reqOut; + + return [cb, { allOutputs, allPropIds }]; + }); + + const ids = callbacks.map(([cb]) => [ + cb.getInputs(paths), + cb.getState(paths), + ]); + + /* Make sure the app is ready to execute callbacks impacting `ids` */ + await isAppReady(layout, paths, uniq(pluck('id', flatten(ids)))); + + const executingCallbacks: IExecutingCallback[] = callbacks.map(([cb, stash]) => { + return executeCallback(cb, config, hooks, paths, layout, stash); + }); + + dispatch(aggregateCallbacks([ + callbacks.length ? removePrioritizedCallbacks(prioritized) : null, + executingCallbacks.length ? addExecutingCallbacks(executingCallbacks) : null + ])); + +}, ['callbacks.prioritized', 'callbacks.completed']); + +observe(({ + dispatch, + getState +}) => { + const { + callbacks: { + executing + }, + } = getState(); + + console.log('onCallbacksChanged.executing', executing); + + 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 + ])); + + deferred.forEach(async function (cb: IExecutingCallback) { + const result = await cb.executionPromise; + + /* Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled */ + const watched = getState().callbacks.watched; + + /* + Find the callback instance or one that matches its promise (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 + }]) + ])); + }); +}, ['callbacks.executing']); + +observe(({ + 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; + } + + console.log('onCallbacksChanged.executed', executed); + + let callbacks: ICallback[] = []; + forEach(({ executionResult }) => { + if (isNil(executionResult)) { + return; + } + + const { data, error } = executionResult; + console.log('SPECIAL', '[executionResult]', data); + + if (data !== undefined) { + return forEach(([id, props]: [any, any]) => { + const parsedId = parseIfWildcard(id); + + // Components will trigger callbacks on their own as required (eg. derived) + const appliedProps = applyProps(parsedId, props); + + // New layout - trigger callbacks for that explicitly + if (has('children', appliedProps)) { + const { children } = appliedProps; + + const { paths: oldPaths, graphs } = getState(); + const childrenPath = concat(getPath(oldPaths, id), ['props', 'children']); + const paths = computePaths(children, childrenPath, oldPaths); + dispatch(setPaths(paths)); + + callbacks = concat( + callbacks, + getCallbacksInLayout(graphs, paths, children, { + chunkPath: childrenPath, + }) + ); + } + + // 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, paths } = getState(); + + callbacks = concat( + callbacks, + includeObservers(id, addedProps, graphs, paths) + ); + } + + }, Object.entries(data)); + } + + if (error !== undefined) { + handleAsyncError(error, error.message, dispatch); + } + }, executed); + + dispatch(aggregateCallbacks([ + executed.length ? removeExecutedCallbacks(executed) : null, + executed.length ? addCompletedCallbacks(executed.length) : null, + callbacks.length ? addRequestedCallbacks(callbacks) : null + ])); +}, ['callbacks.executed']); + +const AppProvider = ({hooks}: any) => { + return ( + + + + ); +}; + +AppProvider.propTypes = { + hooks: PropTypes.shape({ + request_pre: PropTypes.func, + request_post: PropTypes.func, + }), +}; + +AppProvider.defaultProps = { + hooks: { + request_pre: null, + request_post: null, + }, +}; + +export default AppProvider; + + diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index a5a6f016ab..b03cdb0264 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -1,11 +1,8 @@ import { any, - assocPath, - concat, forEach, map, - path, - reduce + path } from 'ramda'; import { Store, Unsubscribe } from 'redux'; @@ -17,7 +14,6 @@ interface IStoreObserver { inputPaths: string[][]; lastState: any; observer: Observer; - statePaths: string[][]; triggered: boolean; } @@ -33,10 +29,9 @@ export default class StoreObserver { observe = ( observer: Observer, - inputs: string[], - states: string[] = [] + inputs: string[] ): UnregisterObserver => { - this.add(observer, inputs, states); + this.add(observer, inputs); return () => this.remove(observer); } @@ -59,13 +54,11 @@ export default class StoreObserver { private add = ( observer: Observer, - inputs: string[], - states: string[] + inputs: string[] ) => this._observers.push({ inputPaths: map(p => p.split('.'), inputs), lastState: null, observer, - statePaths: map(p => p.split('.'), states), triggered: false }); @@ -87,7 +80,7 @@ export default class StoreObserver { return; } - const { inputPaths, lastState, observer, statePaths } = o; + const { inputPaths, lastState, observer } = o; /** Don't notify observer if there's no change */ if (!any( @@ -98,18 +91,15 @@ export default class StoreObserver { } o.triggered = true; - observer({ - ...store, - /** Build partial state that interests the observer */ - getState: () => reduce((s, p) => assocPath( - p, - path(p, state), - s - ), {}, concat(inputPaths, statePaths)) as TStore - }); + /** + * Due to nested store updates, the state could change between the + * observer call and setting `lastState`, leading to untriggered changes + */ + const s = store.getState(); + observer(store); o.triggered = false; - o.lastState = store.getState(); + o.lastState = s; }; private remove = (observer: Observer) => this._observers.splice( diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 3b1c3c13c2..a2437829ab 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -311,8 +311,8 @@ function getLoadingState(layout, pendingCallbacks) { const idStrs = ids.map(stringifyId); pendingCallbacks.forEach(cb => { - const {requestId, requestedOutputs} = cb; - if (requestId === undefined) { + const {executionPromise, requestedOutputs} = cb; + if (executionPromise === undefined) { return; } diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts new file mode 100644 index 0000000000..bd948f1200 --- /dev/null +++ b/dash-renderer/src/actions/callbacks.ts @@ -0,0 +1,359 @@ +import { + concat, + flatten, + keys, + map, + mergeDeepRight, + path, + pick, + pluck, + zip, +} from 'ramda'; + +import { STATUS } from '../constants/constants'; +import { ICallback, CallbackResult, IExecutingCallback, CallbackActionType, IExecutedCallback, CallbackAggregateActionType } from "../reducers/callbacks"; +import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies'; +import { urlBase } from './utils'; +import { getCSRFHeader } from '.'; +import { createAction, Action } from 'redux-actions'; + +export const setPendingCallbacks = createAction('SET_PENDING_CALLBACKS'); + +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 addWatchedCallbacks = createAction(CallbackActionType.AddWatched); +export const removeExecutedCallbacks = createAction( + CallbackActionType.RemoveExecuted +); +export const removeExecutingCallbacks = createAction( + CallbackActionType.RemoveExecuting +); +export const removePrioritizedCallbacks = createAction( + CallbackActionType.RemovePrioritized +); +export const removeRequestedCallbacks = createAction( + CallbackActionType.RemoveRequested +); +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]]); + +export function executeCallback( + cb: ICallback, + 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 => { + let payload: any; + try { + 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 (error) { + resolve({ error }); + } + + function handleClientside(clientside_function: any, payload: any) { + 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)); + } + returnValue = dc[namespace][function_name](...args); + } catch (e) { + if (e === dc.PreventUpdate) { + return {}; + } + throw e; + } + + if (returnValue === 'Object' && returnValue.then) { + 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(payload: any) { + 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; + }); + } + + if (clientside_function) { + try { + resolve({ data: handleClientside(clientside_function, payload) }); + } catch (error) { + resolve({ error }); + } + return null; + } else { + handleServerside(payload) + .then(data => resolve({ data })) + .catch(error => resolve({ error })); + } + }); + + const newCb = { + ...cb, + executionPromise: __promise + }; + + return newCb; + } catch (error) { + return { + ...cb, + executionPromise: { error } + }; + } +} \ No newline at end of file diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index add6a20597..63a5ff9e85 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -7,7 +7,6 @@ import { assoc, clone, difference, - dissoc, equals, evolve, findIndex, @@ -18,14 +17,11 @@ import { isEmpty, keys, map, - mergeDeepRight, mergeRight, mergeWith, - partition, path, - pickBy, pluck, - propEq, + // propEq, props, startsWith, unnest, @@ -91,7 +87,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('.'); @@ -952,23 +948,12 @@ const makeResolvedCallback = (callback, resolve, anyVals) => ({ 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 @@ -1003,10 +988,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. * } @@ -1343,64 +1324,6 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { 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 - ); - 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]) { @@ -1465,95 +1388,3 @@ export function followForward(graphs, paths, callbacks_) { } 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..d46f2e5a26 --- /dev/null +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -0,0 +1,50 @@ +import { + flatten, + keys, + map, + partition, + pickBy, + assoc +} from 'ramda'; +import { ICallback } from '../reducers/callbacks'; +import { getCallbacksByInput, splitIdAndProp } from './dependencies'; +import { getPath } from './paths'; + +export function includeObservers(id: any, props: any, graphs: any, paths: any): ICallback[] { + return flatten(map( + propName => getCallbacksByInput(graphs, paths, id, propName), + keys(props) + )); +} + +export function pruneCallbacks(callbacks: T[], paths: any): { + initial: T[], + modified: T[], + removed: T[], + pruned: number +} { + const [, affected] = partition( + ({ getOutputs, callback: { outputs } }) => flatten(getOutputs(paths)).length === outputs.length, + callbacks + ); + + const [removed, initial] = partition( + ({ getOutputs }) => !flatten(getOutputs(paths)).length, + affected + ); + + const modified = map( + cb => assoc('changedPropIds', pickBy( + (_, propId) => getPath(paths, splitIdAndProp(propId).id), + cb.changedPropIds + ), cb), + initial + ); + + return { + initial, + modified, + removed, + pruned: initial.length + removed.length + }; +} \ No newline at end of file diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 4da204853e..226f0bcc0a 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -1,52 +1,12 @@ -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 { - CallbackActionType, - CallbackAggregateActionType, -} from '../reducers/callbacks'; +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 isAppReady from './isAppReady'; +import {getCallbacksInLayout, validateCallbacksToLayout} from './dependencies'; +import {includeObservers} from './dependencies_ts'; +import {getPath} from './paths'; export const onError = createAction(getAction('ON_ERROR')); export const setAppLifecycle = createAction(getAction('SET_APP_LIFECYCLE')); @@ -55,38 +15,9 @@ export const setGraphs = createAction(getAction('SET_GRAPHS')); export const setHooks = createAction(getAction('SET_HOOKS')); export const setLayout = createAction(getAction('SET_LAYOUT')); export const setPaths = createAction(getAction('SET_PATHS')); -export const setPendingCallbacks = createAction('SET_PENDING_CALLBACKS'); export const setRequestQueue = createAction(getAction('SET_REQUEST_QUEUE')); export const updateProps = createAction(getAction('ON_PROP_CHANGE')); -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 removeExecutedCallbacks = createAction( - CallbackActionType.RemoveExecuted -); -export const removeExecutingCallbacks = createAction( - CallbackActionType.RemoveExecuting -); -export const removePrioritizedCallbacks = createAction( - CallbackActionType.RemovePrioritized -); -export const removeRequestedCallbacks = createAction( - CallbackActionType.RemoveRequested -); -export const aggregateCallbacks = createAction( - CallbackAggregateActionType.Aggregate -); - export const dispatchError = dispatch => (message, lines) => dispatch( onError({ @@ -138,7 +69,8 @@ function triggerDefaultState(dispatch, getState) { const initialCallbacks = getCallbacksInLayout(graphs, paths, layout, { outputsOnly: true, }); - dispatch(startCallbacks(initialCallbacks)); + + dispatch(addRequestedCallbacks(initialCallbacks)); } export const redo = moveHistory('REDO'); @@ -167,555 +99,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; - }); -} - -const getVals = input => - Array.isArray(input) ? pluck('value', input) : input.value; - -const zipIfArray = (a, b) => (Array.isArray(a) ? zip(a, b) : [[a, b]]); - -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 { - 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); - } 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/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index d7cdf728c8..ff9264eaf1 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -4,23 +4,14 @@ import { reduce } from 'ramda'; -/** - * Callback states and transitions - * - * State transition --> {State} - * ------------------------------- - * {Requested} prioritize --> {Prioritized} - * {Prioritized} execute --> {Executing} - * {Executing} processResult --> {Executed} - * {Executed} (none) - */ - export enum CallbackActionType { + AddApplied = 'Callbacks.AddApplied', AddExecuted = 'Callbacks.AddExecuted', AddExecuting = 'Callbacks.AddExecuting', AddPrioritized = 'Callbacks.AddPrioritized', AddRequested = 'Callbacks.AddRequested', - AddWatched = 'Callbacks.Watched', + AddWatched = 'Callbacks.AddWatched', + RemoveApplied = 'Callbacks.RemoveApplied', RemoveExecuted = 'Callbacks.RemoveExecuted', RemoveExecuting = 'Callbacks.RemoveExecuting', RemovePrioritized = 'Callbacks.ReomvePrioritized', @@ -29,35 +20,65 @@ export enum CallbackActionType { } export enum CallbackAggregateActionType { + AddCompleted = 'Callbacks.Completed', Aggregate = 'Callbacks.Aggregate' } -export type CallbackResult = { - data: any; -} | { error: any }; +export interface ICallback { + callback: { + clientside_function: string; + input: string; + inputs: { id: string; property: string; }[]; + output: string; + outputs: { id: string; property: string; }[]; + state: { id: string; property: string; }[]; + }; + prevent_initial_call: boolean; -export type Callback = { - executionResult?: Promise | CallbackResult | null; [key: string]: any; -}; +} + +export interface IExecutingCallback extends ICallback { + executionPromise: Promise | CallbackResult | null; +} -interface ICallbackAction { - type: CallbackActionType | CallbackAggregateActionType | string; - payload: Callback[]; +export interface IExecutedCallback extends IExecutingCallback { + executionResult: CallbackResult | null; } -type CallbackAction = ICallbackAction | { + +export type CallbackResult = { + data?: any; + error?: Error +} + +export interface IAggregateAction { type: CallbackAggregateActionType.Aggregate, - payload: ICallbackAction[] -}; + 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 { - executed: Callback[]; - executing: Callback[]; - prioritized: Callback[]; - requested: Callback[]; - watched: Callback[]; + requested: ICallback[]; + prioritized: ICallback[]; + executing: IExecutingCallback[]; + watched: IExecutingCallback[]; + executed: IExecutedCallback[]; + completed: number; } const DEFAULT_STATE: ICallbacksState = { @@ -65,17 +86,20 @@ const DEFAULT_STATE: ICallbacksState = { executing: [], prioritized: [], requested: [], - watched: [] + watched: [], + completed: 0 }; const transforms: { - [key: string]: (a1: Callback[], a2: Callback[]) => Callback[] + [key: string]: (a1: ICallback[], a2: ICallback[]) => ICallback[] } = { + [CallbackActionType.AddApplied]: concat, [CallbackActionType.AddExecuted]: concat, [CallbackActionType.AddExecuting]: concat, [CallbackActionType.AddPrioritized]: concat, [CallbackActionType.AddRequested]: concat, [CallbackActionType.AddWatched]: concat, + [CallbackActionType.RemoveApplied]: difference, [CallbackActionType.RemoveExecuted]: difference, [CallbackActionType.RemoveExecuting]: difference, [CallbackActionType.RemovePrioritized]: difference, @@ -84,7 +108,7 @@ const transforms: { }; const fields: { - [key: string]: keyof ICallbacksState + [key: string]: keyof Omit } = { [CallbackActionType.AddExecuted]: 'executed', [CallbackActionType.AddExecuting]: 'executing', @@ -98,17 +122,38 @@ const fields: { [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) => { - const transform = transforms[a.type]; - const field = fields[a.type]; - - return (!transform || !field || a.payload.length === 0) ? s : { - ...s, - [field]: transform(s[field], a.payload) - }; + 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/store.ts b/dash-renderer/src/store.ts index 72311180ee..d5a4b7df75 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -2,10 +2,11 @@ import { createStore, applyMiddleware, Store } from 'redux'; import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; -import { ICallbacksState } from './reducers/callbacks'; +import { ICallbacksState, ICallback } from './reducers/callbacks'; interface IStoreState { callbacks: ICallbacksState; + pendingCallbacks: ICallback[]; [key: string]: any; } From a10108573345d8fc7ae2b08cf3be539f8189bb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 May 2020 09:01:10 -0400 Subject: [PATCH 09/90] - extra typing for callbacks / wildcards case - generalized callback props "stringify" --- dash-renderer/src/AppProvider.react.tsx | 44 ++++++++++++++++--------- dash-renderer/src/reducers/callbacks.ts | 13 ++++++-- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 8edc624e35..0536bd79f4 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -11,10 +11,10 @@ import { has, isEmpty, isNil, + keys, map, partition, path, - pick, pickBy, pluck, reduce, @@ -56,9 +56,10 @@ import { } from './actions/callbacks'; import { stringifyId, combineIdAndProp, parseIfWildcard, getCallbacksInLayout } from './actions/dependencies'; import { includeObservers, pruneCallbacks } from './actions/dependencies_ts'; -import { ICallbacksState, IExecutingCallback, ICallback } from './reducers/callbacks'; +import { ICallbacksState, IExecutingCallback, ICallback, ICallbackProperty } from './reducers/callbacks'; import isAppReady from './actions/isAppReady'; import { prunePersistence, applyPersistence } from './persistence'; +import type from 'ramda/es/type'; const store = initializeStore(); @@ -111,6 +112,25 @@ observe(({ console.log('onCallbacksChanged.requested', completed, requested); + const stringifyCallbackProperty = ({ + id, + property + }: ICallbackProperty): string => + type(id) === 'String' ? + `${id}.${property}` : + `{${keys(id).join(',')}}.${property}`; + + const stringifyCallbackProperties = ({ + callback: { + inputs, + outputs, + state } + }: ICallback): string => map(stringifyCallbackProperty, [ + ...inputs, + ...outputs, + ...state + ]).join(','); + /* 1. Remove duplicated `requested` callbacks */ @@ -124,9 +144,7 @@ observe(({ group => group.slice(1), values( groupBy( - r => JSON.stringify( - pick(['inputs', 'outputs', 'state'], r.callback) - ), + stringifyCallbackProperties, requested ) ) @@ -150,9 +168,7 @@ observe(({ group => group.slice(1), values( groupBy( - r => JSON.stringify( - pick(['inputs', 'outputs', 'state'], r.callback) - ), + stringifyCallbackProperties, concat(requested, prioritized) ) ) @@ -162,9 +178,7 @@ observe(({ group => group.slice(1), values( groupBy( - r => JSON.stringify( - pick(['inputs', 'outputs', 'state'], r.callback) - ), + stringifyCallbackProperties, concat(requested, executing) ) ) @@ -174,9 +188,7 @@ observe(({ group => group.slice(1), values( groupBy( - r => JSON.stringify( - pick(['inputs', 'outputs', 'state'], r.callback) - ), + stringifyCallbackProperties, concat(requested, watched) ) ) @@ -214,7 +226,7 @@ observe(({ /* 4. Determine `requested` callbacks that can be `prioritized` */ /* Find all outputs of all active callbacks */ const outputs = map( - o => `${o.id}.${o.property}`, + stringifyCallbackProperty, reduce((o, cb) => concat(o, cb.callback.outputs), [], pendingCallbacks) ); @@ -225,7 +237,7 @@ observe(({ /* Find `requested` callbacks that do not depend on a outstanding output (as either input or state) */ const readyCallbacks = filter( cb => all( - i => !outputsMap[`${i.id}.${i.property}`], + i => !outputsMap[stringifyCallbackProperty(i)], cb.callback.inputs ), requested diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index ff9264eaf1..b0930c327a 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -24,14 +24,21 @@ export enum CallbackAggregateActionType { Aggregate = 'Callbacks.Aggregate' } +type CallbackId = string | { [key: string]: any } + +export interface ICallbackProperty { + id: CallbackId; + property: string; +} + export interface ICallback { callback: { clientside_function: string; input: string; - inputs: { id: string; property: string; }[]; + inputs: ICallbackProperty[]; output: string; - outputs: { id: string; property: string; }[]; - state: { id: string; property: string; }[]; + outputs: ICallbackProperty[]; + state: ICallbackProperty[]; }; prevent_initial_call: boolean; From 549d6233bef35cdc394d92f9a66c86ba18178f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 May 2020 09:22:52 -0400 Subject: [PATCH 10/90] clean up pruning --- dash-renderer/src/AppProvider.react.tsx | 32 ++++++++++---------- dash-renderer/src/actions/dependencies_ts.ts | 22 ++++++-------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 0536bd79f4..d3a6700ba0 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -202,13 +202,13 @@ observe(({ 3. Modify or remove callbacks that are outputing to non-existing layout `id`. */ - const { pruned: rPruned, initial: rInitial, modified: rModified, removed: rRemoved } = pruneCallbacks(requested, paths); - const { pruned: pPruned, initial: pInitial, modified: pModified, removed: pRemoved } = pruneCallbacks(prioritized, paths); - const { pruned: ePruned, initial: eInitial, modified: eModified, removed: eRemoved } = pruneCallbacks(executing, paths); - const { pruned: wPruned, initial: wInitial, modified: wModified, removed: wRemoved } = pruneCallbacks(watched, paths); + const { added: rAdded, removed: rRemoved } = pruneCallbacks(requested, paths); + const { added: pAdded, removed: pRemoved } = pruneCallbacks(prioritized, paths); + const { added: eAdded, removed: eRemoved } = pruneCallbacks(executing, paths); + const { added: wAdded, removed: wRemoved } = pruneCallbacks(watched, paths); - if (rPruned + pPruned + ePruned + wPruned) { - console.log('onCallbacksChanged.requested', '[pruned]', rPruned, pPruned, ePruned, wPruned); + if (rRemoved.length + pRemoved.length + eRemoved.length + wRemoved.length) { + console.log('onCallbacksChanged.requested', '[pruned]', rRemoved.length, pRemoved.length, eRemoved.length, wRemoved.length); } /* @@ -218,9 +218,9 @@ observe(({ requested = concat( difference( requested, - concat(rInitial, rRemoved) + rRemoved ), - rModified + rAdded ); /* 4. Determine `requested` callbacks that can be `prioritized` */ @@ -250,14 +250,14 @@ observe(({ eDuplicates.length ? removeExecutingCallbacks(eDuplicates) : null, wDuplicates.length ? removeWatchedCallbacks(wDuplicates) : null, // Prune callbacks - (rInitial.length + rRemoved.length) ? removeRequestedCallbacks(concat(rInitial, rRemoved)) : null, - rModified.length ? addRequestedCallbacks(rModified) : null, - (pInitial.length + pRemoved.length) ? removePrioritizedCallbacks(concat(pInitial, pRemoved)) : null, - pModified.length ? addPrioritizedCallbacks(pModified) : null, - (eInitial.length + eRemoved.length) ? removeExecutingCallbacks(concat(eInitial, eRemoved)) : null, - eModified.length ? addExecutingCallbacks(eModified) : null, - (wInitial.length + wRemoved.length) ? removeWatchedCallbacks(concat(wInitial, wRemoved)) : null, - wModified.length ? addWatchedCallbacks(wModified) : null, + rRemoved.length ? removeRequestedCallbacks(rRemoved) : null, + rAdded.length ? addRequestedCallbacks(rAdded) : null, + pRemoved.length ? removePrioritizedCallbacks(pRemoved) : null, + pAdded.length ? addPrioritizedCallbacks(pAdded) : null, + eRemoved.length ? removeExecutingCallbacks(eRemoved) : null, + eAdded.length ? addExecutingCallbacks(eAdded) : null, + wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, + wAdded.length ? addWatchedCallbacks(wAdded) : null, // Promoted callbacks readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index d46f2e5a26..3e16b5f2bd 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -18,33 +18,29 @@ export function includeObservers(id: any, props: any, graphs: any, paths: any): } export function pruneCallbacks(callbacks: T[], paths: any): { - initial: T[], - modified: T[], - removed: T[], - pruned: number + added: T[], + removed: T[] } { - const [, affected] = partition( + const [, removed] = partition( ({ getOutputs, callback: { outputs } }) => flatten(getOutputs(paths)).length === outputs.length, callbacks ); - const [removed, initial] = partition( + const [, modified] = partition( ({ getOutputs }) => !flatten(getOutputs(paths)).length, - affected + removed ); - const modified = map( + const added = map( cb => assoc('changedPropIds', pickBy( (_, propId) => getPath(paths, splitIdAndProp(propId).id), cb.changedPropIds ), cb), - initial + modified ); return { - initial, - modified, - removed, - pruned: initial.length + removed.length + added, + removed }; } \ No newline at end of file From c7e74eca7d3d221be4409740b270977ce4ef5fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 May 2020 16:17:42 -0400 Subject: [PATCH 11/90] followForward on prop update --- dash-renderer/src/actions/dependencies.js | 75 +------------------- dash-renderer/src/actions/dependencies_ts.ts | 48 +++++++++++-- dash-renderer/src/reducers/callbacks.ts | 3 + 3 files changed, 47 insertions(+), 79 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 63a5ff9e85..d811cc59f4 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -5,7 +5,6 @@ import { any, ap, assoc, - clone, difference, equals, evolve, @@ -18,20 +17,16 @@ import { keys, map, mergeRight, - mergeWith, path, pluck, - // propEq, props, startsWith, - unnest, values, zip, zipObj, } from 'ramda'; -const mergeMax = mergeWith(Math.max); - +import {DIRECT, followForward, INDIRECT, mergeMax} from './dependencies_ts'; import {computePaths, getPath} from './paths'; import {crawlLayout} from './utils'; @@ -951,9 +946,6 @@ const makeResolvedCallback = (callback, resolve, anyVals) => ({ requestedOutputs: {}, }); -const DIRECT = 2; -const INDIRECT = 1; - /* * Does this item (input / output / state) support multiple values? * string IDs do not; wildcard IDs only do if they contain ALL or ALLSMALLER @@ -1323,68 +1315,3 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { return finalCallbacks; } - -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; -} diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 3e16b5f2bd..db37cd4fe0 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -1,20 +1,27 @@ import { + assoc, + concat, flatten, keys, map, + mergeWith, partition, pickBy, - assoc + unnest } from 'ramda'; -import { ICallback } from '../reducers/callbacks'; +import { ICallback, ICallbackProperty } from '../reducers/callbacks'; import { getCallbacksByInput, splitIdAndProp } from './dependencies'; import { getPath } from './paths'; +export const DIRECT = 2; +export const INDIRECT = 1; +export const mergeMax = mergeWith(Math.max); + export function includeObservers(id: any, props: any, graphs: any, paths: any): ICallback[] { - return flatten(map( + return followForward(graphs, paths, flatten(map( propName => getCallbacksByInput(graphs, paths, id, propName), keys(props) - )); + ))); } export function pruneCallbacks(callbacks: T[], paths: any): { @@ -43,4 +50,35 @@ export function pruneCallbacks(callbacks: T[], paths: any): added, removed }; -} \ No newline at end of file +} + +/* + * 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: any, paths: any, callbacks: ICallback[]): ICallback[] { + callbacks = callbacks.slice(0); + let i; + let callback: ICallback; + + const followOutput = ({ id, property }: ICallbackProperty) => { + callbacks = concat(callbacks, getCallbacksByInput( + graphs, + paths, + id, + property, + INDIRECT + )); + }; + + // 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; +} diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index b0930c327a..46748ac510 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -40,6 +40,9 @@ export interface ICallback { outputs: ICallbackProperty[]; state: ICallbackProperty[]; }; + getInputs: (paths: any) => ICallbackProperty[]; + getOutputs: (paths: any) => ICallbackProperty[]; + getState: (paths: any) => ICallbackProperty[]; prevent_initial_call: boolean; [key: string]: any; From e9494e4b4234711332d54f027b59e7ab7c6d1d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 13 May 2020 17:31:15 -0400 Subject: [PATCH 12/90] use parsedId --- dash-renderer/src/AppProvider.react.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index d3a6700ba0..a8efef3c8f 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -301,13 +301,14 @@ observe(async ({ return [cb, { allOutputs, allPropIds }]; }); - const ids = callbacks.map(([cb]) => [ - cb.getInputs(paths), - cb.getState(paths), - ]); + const ids = reduce((res, [cb]) => ([ + ...res, + ...cb.getInputs(paths), + ...cb.getState(paths) + ]), [] as ICallbackProperty[], callbacks); /* Make sure the app is ready to execute callbacks impacting `ids` */ - await isAppReady(layout, paths, uniq(pluck('id', flatten(ids)))); + await isAppReady(layout, paths, uniq(pluck('id', ids))); const executingCallbacks: IExecutingCallback[] = callbacks.map(([cb, stash]) => { return executeCallback(cb, config, hooks, paths, layout, stash); @@ -418,7 +419,7 @@ observe(({ console.log('SPECIAL', '[executionResult]', data); if (data !== undefined) { - return forEach(([id, props]: [any, any]) => { + return forEach(([id, props]: [any, { [key: string]: any }]) => { const parsedId = parseIfWildcard(id); // Components will trigger callbacks on their own as required (eg. derived) @@ -429,7 +430,7 @@ observe(({ const { children } = appliedProps; const { paths: oldPaths, graphs } = getState(); - const childrenPath = concat(getPath(oldPaths, id), ['props', 'children']); + const childrenPath = concat(getPath(oldPaths, parsedId), ['props', 'children']); const paths = computePaths(children, childrenPath, oldPaths); dispatch(setPaths(paths)); From 27044c26b22832636d94fc56697ae44fb1039fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 10:27:17 -0400 Subject: [PATCH 13/90] fix regressions with wildcard callbacks duplicates handling --- dash-renderer/src/AppProvider.react.tsx | 54 ++++++-------------- dash-renderer/src/actions/dependencies.js | 11 ++-- dash-renderer/src/actions/dependencies_ts.ts | 25 ++++++++- dash-renderer/src/reducers/callbacks.ts | 1 + 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index a8efef3c8f..bf8ebf4f41 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -11,7 +11,6 @@ import { has, isEmpty, isNil, - keys, map, partition, path, @@ -35,31 +34,28 @@ import { handleAsyncError } from './actions'; import { + addCompletedCallbacks, addExecutedCallbacks, addExecutingCallbacks, addPrioritizedCallbacks, + addRequestedCallbacks, + addWatchedCallbacks, aggregateCallbacks, + executeCallback, + removeExecutedCallbacks, + removeExecutingCallbacks, removePrioritizedCallbacks, removeRequestedCallbacks, - setPendingCallbacks, - addWatchedCallbacks, - removeExecutingCallbacks, removeWatchedCallbacks, - removeExecutedCallbacks, - addCompletedCallbacks, - addRequestedCallbacks + setPendingCallbacks } from './actions/callbacks'; import { getPath, computePaths } from './actions/paths'; -import { - executeCallback -} from './actions/callbacks'; -import { stringifyId, combineIdAndProp, parseIfWildcard, getCallbacksInLayout } from './actions/dependencies'; -import { includeObservers, pruneCallbacks } from './actions/dependencies_ts'; +import { stringifyId, parseIfWildcard, getCallbacksInLayout } from './actions/dependencies'; +import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks } from './actions/dependencies_ts'; import { ICallbacksState, IExecutingCallback, ICallback, ICallbackProperty } from './reducers/callbacks'; import isAppReady from './actions/isAppReady'; import { prunePersistence, applyPersistence } from './persistence'; -import type from 'ramda/es/type'; const store = initializeStore(); @@ -112,30 +108,10 @@ observe(({ console.log('onCallbacksChanged.requested', completed, requested); - const stringifyCallbackProperty = ({ - id, - property - }: ICallbackProperty): string => - type(id) === 'String' ? - `${id}.${property}` : - `{${keys(id).join(',')}}.${property}`; - - const stringifyCallbackProperties = ({ - callback: { - inputs, - outputs, - state } - }: ICallback): string => map(stringifyCallbackProperty, [ - ...inputs, - ...outputs, - ...state - ]).join(','); - /* 1. Remove duplicated `requested` callbacks */ - /* Extract all but the first callback from each IOS-key group these callbacks are duplicates. @@ -144,7 +120,7 @@ observe(({ group => group.slice(1), values( groupBy( - stringifyCallbackProperties, + getUniqueIdentifier, requested ) ) @@ -168,7 +144,7 @@ observe(({ group => group.slice(1), values( groupBy( - stringifyCallbackProperties, + getUniqueIdentifier, concat(requested, prioritized) ) ) @@ -178,7 +154,7 @@ observe(({ group => group.slice(1), values( groupBy( - stringifyCallbackProperties, + getUniqueIdentifier, concat(requested, executing) ) ) @@ -188,7 +164,7 @@ observe(({ group => group.slice(1), values( groupBy( - stringifyCallbackProperties, + getUniqueIdentifier, concat(requested, watched) ) ) @@ -226,7 +202,7 @@ observe(({ /* 4. Determine `requested` callbacks that can be `prioritized` */ /* Find all outputs of all active callbacks */ const outputs = map( - stringifyCallbackProperty, + combineIdAndProp, reduce((o, cb) => concat(o, cb.callback.outputs), [], pendingCallbacks) ); @@ -237,7 +213,7 @@ observe(({ /* Find `requested` callbacks that do not depend on a outstanding output (as either input or state) */ const readyCallbacks = filter( cb => all( - i => !outputsMap[stringifyCallbackProperty(i)], + cbp => !outputsMap[combineIdAndProp(cbp)], cb.callback.inputs ), requested diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index d811cc59f4..aaaa1798cd 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -26,7 +26,13 @@ import { zipObj, } from 'ramda'; -import {DIRECT, followForward, INDIRECT, mergeMax} from './dependencies_ts'; +import { + combineIdAndProp, + DIRECT, + followForward, + INDIRECT, + mergeMax, +} from './dependencies_ts'; import {computePaths, getPath} from './paths'; import {crawlLayout} from './utils'; @@ -100,9 +106,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 */ diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index db37cd4fe0..fe92d18d37 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -10,13 +10,36 @@ import { unnest } from 'ramda'; import { ICallback, ICallbackProperty } from '../reducers/callbacks'; -import { getCallbacksByInput, splitIdAndProp } from './dependencies'; +import { getCallbacksByInput, splitIdAndProp, stringifyId } 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 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, props: any, graphs: any, paths: any): ICallback[] { return followForward(graphs, paths, flatten(map( propName => getCallbacksByInput(graphs, paths, id, propName), diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 46748ac510..2d10f8101a 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -32,6 +32,7 @@ export interface ICallbackProperty { } export interface ICallback { + anyVals: any[] | string; callback: { clientside_function: string; input: string; From 4e8bb3e350b86fb11b8fabf5f6dace8e82f9350f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 11:55:16 -0400 Subject: [PATCH 14/90] add back handling `children` prop case with wildcards --- dash-renderer/src/AppProvider.react.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index bf8ebf4f41..c2b1821803 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -397,6 +397,7 @@ observe(({ if (data !== undefined) { return 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); @@ -405,15 +406,20 @@ observe(({ if (has('children', appliedProps)) { const { children } = appliedProps; - const { paths: oldPaths, graphs } = getState(); - const childrenPath = concat(getPath(oldPaths, parsedId), ['props', 'children']); - const paths = computePaths(children, childrenPath, oldPaths); + 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)); callbacks = concat( - callbacks, getCallbacksInLayout(graphs, paths, children, { - chunkPath: childrenPath, + chunkPath: oldChildrenPath, + }), + // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger + // even due to the deletion of components + getCallbacksInLayout(graphs, oldPaths, oldChildren, { + removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath }) ); } From d76111e6a0546b9b5d60ffb594acd6cc10bd92ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 15:02:05 -0400 Subject: [PATCH 15/90] always concat --- dash-renderer/src/AppProvider.react.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index c2b1821803..5cfe0736ff 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -413,11 +413,16 @@ observe(({ dispatch(setPaths(paths)); callbacks = concat( + callbacks, getCallbacksInLayout(graphs, paths, children, { chunkPath: oldChildrenPath, - }), - // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger - // even due to the deletion of components + }) + ); + + // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger + // even due to the deletion of components + callbacks = concat( + callbacks, getCallbacksInLayout(graphs, oldPaths, oldChildren, { removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath }) From c5778d710515a31d61b4c44f0862fd690dccfb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 18:01:38 -0400 Subject: [PATCH 16/90] followForward on callback resolution --- dash-renderer/src/AppProvider.react.tsx | 11 +++++- dash-renderer/src/actions/dependencies.js | 28 +------------- dash-renderer/src/actions/dependencies_ts.ts | 40 ++------------------ 3 files changed, 15 insertions(+), 64 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 5cfe0736ff..5b71870376 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -11,6 +11,7 @@ import { has, isEmpty, isNil, + keys, map, partition, path, @@ -51,7 +52,7 @@ import { } from './actions/callbacks'; import { getPath, computePaths } from './actions/paths'; -import { stringifyId, parseIfWildcard, getCallbacksInLayout } from './actions/dependencies'; +import { stringifyId, parseIfWildcard, getCallbacksInLayout, getCallbacksByInput } from './actions/dependencies'; import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks } from './actions/dependencies_ts'; import { ICallbacksState, IExecutingCallback, ICallback, ICallbackProperty } from './reducers/callbacks'; import isAppReady from './actions/isAppReady'; @@ -402,6 +403,14 @@ observe(({ // Components will trigger callbacks on their own as required (eg. derived) const appliedProps = applyProps(parsedId, props); + callbacks = concat( + callbacks, + flatten(map( + prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop), + keys(props) + )) + ) + // New layout - trigger callbacks for that explicitly if (has('children', appliedProps)) { const { children } = appliedProps; diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index aaaa1798cd..5398e09a37 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -29,7 +29,6 @@ import { import { combineIdAndProp, DIRECT, - followForward, INDIRECT, mergeMax, } from './dependencies_ts'; @@ -1291,30 +1290,5 @@ 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; + return callbacks; } diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index fe92d18d37..55bc7fee85 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -6,8 +6,7 @@ import { map, mergeWith, partition, - pickBy, - unnest + pickBy } from 'ramda'; import { ICallback, ICallbackProperty } from '../reducers/callbacks'; import { getCallbacksByInput, splitIdAndProp, stringifyId } from './dependencies'; @@ -41,10 +40,10 @@ export const getUniqueIdentifier = ({ ).join(','); export function includeObservers(id: any, props: any, graphs: any, paths: any): ICallback[] { - return followForward(graphs, paths, flatten(map( + return flatten(map( propName => getCallbacksByInput(graphs, paths, id, propName), keys(props) - ))); + )); } export function pruneCallbacks(callbacks: T[], paths: any): { @@ -73,35 +72,4 @@ export function pruneCallbacks(callbacks: T[], paths: any): added, removed }; -} - -/* - * 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: any, paths: any, callbacks: ICallback[]): ICallback[] { - callbacks = callbacks.slice(0); - let i; - let callback: ICallback; - - const followOutput = ({ id, property }: ICallbackProperty) => { - callbacks = concat(callbacks, getCallbacksByInput( - graphs, - paths, - id, - property, - INDIRECT - )); - }; - - // 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; -} +} \ No newline at end of file From 211b48c74049f82b8ea05ac5bb88850bcb29e665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 18:02:35 -0400 Subject: [PATCH 17/90] lint --- dash-renderer/src/actions/dependencies.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 5398e09a37..1809d5c2dc 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -26,12 +26,7 @@ import { zipObj, } from 'ramda'; -import { - combineIdAndProp, - DIRECT, - INDIRECT, - mergeMax, -} from './dependencies_ts'; +import {combineIdAndProp, DIRECT, INDIRECT, mergeMax} from './dependencies_ts'; import {computePaths, getPath} from './paths'; import {crawlLayout} from './utils'; From 7d72c8e9564405faedc5ad2f7dc1fb155792ac48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 20:19:30 -0400 Subject: [PATCH 18/90] during initialization, incomplete callbacks do not prevent partial/complete ones from firing --- .../callbacks/test_missing_inputs.py | 17 +++++++++-------- .../callbacks/test_multiple_callbacks.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/integration/callbacks/test_missing_inputs.py b/tests/integration/callbacks/test_missing_inputs.py index 2cfffb8ecc..28b5fec7bd 100644 --- a/tests/integration/callbacks/test_missing_inputs.py +++ b/tests/integration/callbacks/test_missing_inputs.py @@ -62,14 +62,15 @@ def out3(out1, title): dash_duo.start_server(app) wait_for_queue(dash_duo) - # out3 fires because it has another Input besides out1 + # out3 fires because it has an existing input dash_duo.wait_for_text_to_equal("#out3", "output1 init - 3 - Title") assert dash_duo.find_element("#out1").text == "output1 init" - # out2 doesn't fire because its only input (out1) is "prevented" + # out2 is fired because while its only input (out1) is "prevented", + # it exists and provides a value for it to execute # State items don't matter for this. - assert dash_duo.find_element("#out2").text == "output2 init" + assert dash_duo.find_element("#out2").text == "output1 init - 2 - Title" dash_duo.find_element("#btn").click() @@ -114,9 +115,9 @@ def out3(out1, out2): dash_duo.start_server(app) wait_for_queue(dash_duo) - - for i in ["1", "2", "3"]: - assert dash_duo.find_element("#out" + i).text == "output{} init".format(i) + assert dash_duo.find_element("#out1").text == "output1 init" + assert dash_duo.find_element("#out2").text == "output2 init" + assert dash_duo.find_element("#out3").text == "output1 initoutput2 init" dash_duo.find_element("#btn").click() # now all callbacks fire @@ -383,7 +384,7 @@ def out2(out1): dash_duo.wait_for_text_to_equal("#content", "content init") assert dash_duo.find_element("#out1").text == "output1 init" - assert dash_duo.find_element("#out2").text == "output2 init" + assert dash_duo.find_element("#out2").text == "output1 init - 2" dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal("#out1", "A - item 0") @@ -472,7 +473,7 @@ def cssid(v): # out2 doesn't fire because its only input (out1) is "prevented" # State items don't matter for this. - assert dash_duo.find_element(cssid("out2")).text == "output2 init" + assert dash_duo.find_element(cssid("out2")).text == "output1 init - 2 - Title" dash_duo.find_element("#btn").click() diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index d9b13503a0..582ef87983 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -184,8 +184,8 @@ def update_slider2_label(val): dash_duo.start_server(app) - dash_duo.wait_for_text_to_equal("#label1", "") - dash_duo.wait_for_text_to_equal("#label2", "") + dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 0") + dash_duo.wait_for_text_to_equal("#label2", "Slider1 value 0") dash_duo.find_element("#button").click() dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 1") From 0dfc2e095ce593322fd4330f45c3c56538414094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 14 May 2020 20:52:31 -0400 Subject: [PATCH 19/90] fix typo --- tests/integration/callbacks/test_multiple_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index 582ef87983..a63f1b714d 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -185,7 +185,7 @@ def update_slider2_label(val): dash_duo.start_server(app) dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 0") - dash_duo.wait_for_text_to_equal("#label2", "Slider1 value 0") + dash_duo.wait_for_text_to_equal("#label2", "Slider2 value 0") dash_duo.find_element("#button").click() dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 1") From 9bc4b739ef1f77b115433ed8e8158fb76795f1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 08:23:59 -0400 Subject: [PATCH 20/90] fix regression in dvcv008/009 --- dash-renderer/src/AppProvider.react.tsx | 36 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 5b71870376..4d2bc8617d 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -9,6 +9,7 @@ import { forEach, groupBy, has, + includes, isEmpty, isNil, keys, @@ -78,7 +79,7 @@ observe(({ } = getState(); const next = getPendingCallbacks(callbacks); - console.log('onCallbacksChanged', '[pendingCallbacks-candidate]', next); + console.log('onCallbacksChanged', '[pendingCallbacks-candidate]', callbacks, next); /** * If the calculated list of pending callbacks is equivalent @@ -284,15 +285,30 @@ observe(async ({ ...cb.getState(paths) ]), [] as ICallbackProperty[], callbacks); - /* Make sure the app is ready to execute callbacks impacting `ids` */ + /* + Make sure the app is ready to execute callbacks impacting `ids` + */ await isAppReady(layout, paths, uniq(pluck('id', ids))); - const executingCallbacks: IExecutingCallback[] = callbacks.map(([cb, stash]) => { + /* + Make sure to only execute callbacks that are still in the `prioritized` list (isAppReady is async - state could have changed) + */ + const { callbacks: { prioritized: updatedPrioritized } } = getState(); + const [remainingCallbacks, droppedCallbacks] = partition( + ([cb]) => includes(cb, updatedPrioritized), + callbacks + ); + + if (droppedCallbacks.length) { + console.log('onCallbacksChanged.prioritized', '[dropped]', map(([cb]) => cb, droppedCallbacks)); + } + + const executingCallbacks: IExecutingCallback[] = remainingCallbacks.map(([cb, stash]) => { return executeCallback(cb, config, hooks, paths, layout, stash); }); dispatch(aggregateCallbacks([ - callbacks.length ? removePrioritizedCallbacks(prioritized) : null, + remainingCallbacks.length ? removePrioritizedCallbacks(map(([cb]) => cb, remainingCallbacks)) : null, executingCallbacks.length ? addExecutingCallbacks(executingCallbacks) : null ])); @@ -321,18 +337,22 @@ observe(({ deferred.forEach(async function (cb: IExecutingCallback) { const result = await cb.executionPromise; - /* Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled */ - const watched = getState().callbacks.watched; + /* + Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled + */ + const { callbacks: { watched } } = getState(); /* - Find the callback instance or one that matches its promise (could have been pruned) + 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` */ + /* + Otherwise move to `executed` and remove from `watched` + */ dispatch(aggregateCallbacks([ removeWatchedCallbacks([currentCb]), addExecutedCallbacks([{ From 3f1667643071eaf33f58e09409d68ef105aa569d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 08:50:04 -0400 Subject: [PATCH 21/90] Expect `callback2` to be triggered on initialization despite `callback1` not being triggered --- tests/integration/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 8476f9fb90..427aa04b67 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -156,8 +156,8 @@ def callback2(value): ) assert ( - callback2_count.value == 0 - ), "callback2 is never triggered, even on initial load" + callback2_count.value == 1 + ), "callback2 is triggered on initial load, even if callback1 is prevented" # double check that output1 and output2 children were not updated assert dash_duo.find_element("#output1").text == initial_output From c9f721430acf844a67ecd0496dbce24b767c0743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 09:25:57 -0400 Subject: [PATCH 22/90] fix clsd005 regression: clientside promise case was mishandled --- dash-renderer/src/actions/callbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index bd948f1200..298819d276 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -271,7 +271,7 @@ export function executeCallback( throw e; } - if (returnValue === 'Object' && returnValue.then) { + if (typeof returnValue?.then === 'function') { throw new Error( 'The clientside function returned a Promise. ' + 'Promises are not supported in Dash clientside ' + From d8f9b6354464fbf7da08eee5530c1205dcd2d9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 09:27:07 -0400 Subject: [PATCH 23/90] cbmt005: now expecting callback to be triggered at initialization even if predecessor is prevented or fully incomplete --- tests/integration/callbacks/test_multiple_callbacks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index a63f1b714d..f5a5645d16 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -226,6 +226,9 @@ def update_sliders(button1, button2): [Input("slider1", "value"), Input("slider2", "value")], ) def update_graph(s1, s2): + if s1 is None or s2 is None: + raise PreventUpdate + return "x={}, y={}".format(s1, s2) dash_duo.start_server(app) From cbdd27750bc474a38304a0983bf14606c29f3c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 09:37:12 -0400 Subject: [PATCH 24/90] fix regression on dvcv012: Prune circular dependencies from `requested` callbacks --- dash-renderer/src/AppProvider.react.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 4d2bc8617d..d043f55355 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -103,7 +103,7 @@ observe(({ dispatch, getState }) => { - const { callbacks, callbacks: { prioritized, executing, watched, completed }, paths } = getState(); + const { callbacks, callbacks: { prioritized, executing, watched, executed, completed }, paths } = getState(); let { callbacks: { requested } } = getState(); const pendingCallbacks = getPendingCallbacks(callbacks); @@ -221,6 +221,24 @@ observe(({ requested ); + /* + If: + - there are `requested` callbacks + - no `requested` callback can be promoted to `prioritized` + - no callbacks are `prioritized`, `executing`, `watched` or `executed` + Then: + - the `requested` callbacks form a ciruclar dependency and can never be executed + - prune them out of `requested` + */ + const rCircular = ( + !readyCallbacks.length && + !prioritized.length && + !executing.length && + !watched.length && + !executed.length && + requested.length + ) ? requested : []; + dispatch(aggregateCallbacks([ // Clean up duplicated callbacks rDuplicates.length ? removeRequestedCallbacks(rDuplicates) : null, @@ -236,7 +254,9 @@ observe(({ eAdded.length ? addExecutingCallbacks(eAdded) : null, wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, wAdded.length ? addWatchedCallbacks(wAdded) : null, - // Promoted callbacks + // Prune circular callbacks + rCircular.length ? removeRequestedCallbacks(rCircular) : null, + // Promote callbacks readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null ])); From e8985c4b25164257e1caa703401338bfacfae3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 09:54:34 -0400 Subject: [PATCH 25/90] group callback related types in a file to that effect --- dash-renderer/src/AppProvider.react.tsx | 3 +- dash-renderer/src/actions/callbacks.ts | 3 +- dash-renderer/src/actions/dependencies_ts.ts | 2 +- dash-renderer/src/reducers/callbacks.ts | 45 +++----------------- dash-renderer/src/store.ts | 3 +- dash-renderer/src/types/callbacks.ts | 37 ++++++++++++++++ 6 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 dash-renderer/src/types/callbacks.ts diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index d043f55355..161f273618 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -55,7 +55,8 @@ import { getPath, computePaths } from './actions/paths'; import { stringifyId, parseIfWildcard, getCallbacksInLayout, getCallbacksByInput } from './actions/dependencies'; import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks } from './actions/dependencies_ts'; -import { ICallbacksState, IExecutingCallback, ICallback, ICallbackProperty } from './reducers/callbacks'; +import { ICallbacksState } from './reducers/callbacks'; +import { IExecutingCallback, ICallback, ICallbackProperty } from './types/callbacks'; import isAppReady from './actions/isAppReady'; import { prunePersistence, applyPersistence } from './persistence'; diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 298819d276..3df508bbdc 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -11,7 +11,8 @@ import { } from 'ramda'; import { STATUS } from '../constants/constants'; -import { ICallback, CallbackResult, IExecutingCallback, CallbackActionType, IExecutedCallback, CallbackAggregateActionType } from "../reducers/callbacks"; +import { CallbackActionType, CallbackAggregateActionType } from "../reducers/callbacks"; +import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback } from '../types/callbacks'; import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies'; import { urlBase } from './utils'; import { getCSRFHeader } from '.'; diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 55bc7fee85..ee93a065d6 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -8,7 +8,7 @@ import { partition, pickBy } from 'ramda'; -import { ICallback, ICallbackProperty } from '../reducers/callbacks'; +import { ICallback, ICallbackProperty } from '../types/callbacks'; import { getCallbacksByInput, splitIdAndProp, stringifyId } from './dependencies'; import { getPath } from './paths'; diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 2d10f8101a..820d2f1e3b 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -4,6 +4,12 @@ import { reduce } from 'ramda'; +import { + ICallback, + IExecutedCallback, + IExecutingCallback +} from '../types/callbacks'; + export enum CallbackActionType { AddApplied = 'Callbacks.AddApplied', AddExecuted = 'Callbacks.AddExecuted', @@ -24,45 +30,6 @@ export enum CallbackAggregateActionType { Aggregate = 'Callbacks.Aggregate' } -type CallbackId = string | { [key: string]: any } - -export interface ICallbackProperty { - id: CallbackId; - property: string; -} - -export interface ICallback { - anyVals: any[] | string; - callback: { - clientside_function: string; - input: string; - inputs: ICallbackProperty[]; - output: string; - outputs: ICallbackProperty[]; - state: ICallbackProperty[]; - }; - getInputs: (paths: any) => ICallbackProperty[]; - getOutputs: (paths: any) => ICallbackProperty[]; - getState: (paths: any) => ICallbackProperty[]; - prevent_initial_call: boolean; - - [key: string]: any; -} - -export interface IExecutingCallback extends ICallback { - executionPromise: Promise | CallbackResult | null; -} - -export interface IExecutedCallback extends IExecutingCallback { - executionResult: CallbackResult | null; -} - - -export type CallbackResult = { - data?: any; - error?: Error -} - export interface IAggregateAction { type: CallbackAggregateActionType.Aggregate, payload: (ICallbackAction | ICompletedAction | null)[] diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index d5a4b7df75..9d1c347ac8 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -2,7 +2,8 @@ import { createStore, applyMiddleware, Store } from 'redux'; import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; -import { ICallbacksState, ICallback } from './reducers/callbacks'; +import { ICallbacksState } from './reducers/callbacks'; +import { ICallback } from './types/callbacks'; interface IStoreState { callbacks: ICallbacksState; diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts new file mode 100644 index 0000000000..5a1d153818 --- /dev/null +++ b/dash-renderer/src/types/callbacks.ts @@ -0,0 +1,37 @@ +type CallbackId = string | { [key: string]: any } + +export interface ICallbackProperty { + id: CallbackId; + property: string; +} + +export interface ICallback { + anyVals: any[] | string; + callback: { + clientside_function: string; + input: string; + inputs: ICallbackProperty[]; + output: string; + outputs: ICallbackProperty[]; + state: ICallbackProperty[]; + }; + getInputs: (paths: any) => ICallbackProperty[]; + getOutputs: (paths: any) => ICallbackProperty[]; + getState: (paths: any) => ICallbackProperty[]; + prevent_initial_call: boolean; + + [key: string]: any; +} + +export interface IExecutingCallback extends ICallback { + executionPromise: Promise | CallbackResult | null; +} + +export interface IExecutedCallback extends IExecutingCallback { + executionResult: CallbackResult | null; +} + +export type CallbackResult = { + data?: any; + error?: Error +} \ No newline at end of file From 84d1eb894e830cbde8b3375165ccb9ed00054d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 10:03:53 -0400 Subject: [PATCH 26/90] rdmo004: update test to reflect circular dependencies are not executed --- tests/integration/renderer/test_multi_output.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 4741471420..71f4874aba 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -1,5 +1,4 @@ from multiprocessing import Value - import dash from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate @@ -133,10 +132,8 @@ def set_bc(a): dev_tools_hot_reload=False, ) - # the UI still renders the output triggered by callback. - # The new system does NOT loop infinitely like it used to, each callback - # is invoked no more than once. - dash_duo.wait_for_text_to_equal("#c", "X") + # The new system does NOT trigger callbacks in circular dependencies + dash_duo.wait_for_text_to_equal("#c", "") err_text = dash_duo.find_element("span.dash-fe-error__title").text assert err_text == "Circular Dependencies" From 762f55b4d7cfc131bc93b9dfb1f66688fe1e4a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 16:29:18 -0400 Subject: [PATCH 27/90] - revert `layout` initialization and `children` props behavior so it is identical to baseline - revert related tests --- dash-renderer/src/AppProvider.react.tsx | 33 ++++---------- dash-renderer/src/actions/dependencies_ts.ts | 44 ++++++++++++++++++- dash-renderer/src/actions/index.js | 16 ++++--- .../callbacks/test_missing_inputs.py | 17 ++++--- .../callbacks/test_multiple_callbacks.py | 9 ++-- .../integration/renderer/test_multi_output.py | 1 + tests/integration/test_integration.py | 4 +- 7 files changed, 73 insertions(+), 51 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 161f273618..43873424e1 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -1,9 +1,7 @@ import { - all, assoc, concat, difference, - filter, find, flatten, forEach, @@ -53,8 +51,8 @@ import { } from './actions/callbacks'; import { getPath, computePaths } from './actions/paths'; -import { stringifyId, parseIfWildcard, getCallbacksInLayout, getCallbacksByInput } from './actions/dependencies'; -import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks } from './actions/dependencies_ts'; +import { stringifyId, parseIfWildcard, getCallbacksByInput } from './actions/dependencies'; +import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks, getReadyCallbacks, getLayoutCallbacks } from './actions/dependencies_ts'; import { ICallbacksState } from './reducers/callbacks'; import { IExecutingCallback, ICallback, ICallbackProperty } from './types/callbacks'; import isAppReady from './actions/isAppReady'; @@ -202,25 +200,10 @@ observe(({ rAdded ); - /* 4. Determine `requested` callbacks that can be `prioritized` */ - /* Find all outputs of all active callbacks */ - const outputs = map( - combineIdAndProp, - reduce((o, cb) => concat(o, cb.callback.outputs), [], pendingCallbacks) - ); - - /* 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) */ - const readyCallbacks = filter( - cb => all( - cbp => !outputsMap[combineIdAndProp(cbp)], - cb.callback.inputs - ), - requested - ); + /* + 4. Find `requested` callbacks that do not depend on a outstanding output (as either input or state) + */ + const readyCallbacks = getReadyCallbacks(requested, pendingCallbacks); /* If: @@ -464,7 +447,7 @@ observe(({ callbacks = concat( callbacks, - getCallbacksInLayout(graphs, paths, children, { + getLayoutCallbacks(graphs, paths, children, { chunkPath: oldChildrenPath, }) ); @@ -473,7 +456,7 @@ observe(({ // even due to the deletion of components callbacks = concat( callbacks, - getCallbacksInLayout(graphs, oldPaths, oldChildren, { + getLayoutCallbacks(graphs, oldPaths, oldChildren, { removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath }) ); diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index ee93a065d6..90e6194369 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -1,15 +1,19 @@ import { + all, assoc, concat, + filter, flatten, + forEach, keys, map, mergeWith, partition, - pickBy + pickBy, + reduce } from 'ramda'; import { ICallback, ICallbackProperty } from '../types/callbacks'; -import { getCallbacksByInput, splitIdAndProp, stringifyId } from './dependencies'; +import { getCallbacksByInput, splitIdAndProp, stringifyId, getCallbacksInLayout } from './dependencies'; import { getPath } from './paths'; export const DIRECT = 2; @@ -21,6 +25,42 @@ export const combineIdAndProp = ({ property }: ICallbackProperty) => `${stringifyId(id)}.${property}`; +export const getReadyCallbacks = ( + candidates: ICallback[], + callbacks: ICallback[] = candidates +): ICallback[] => { + // Find all outputs of all active callbacks + const outputs = map( + combineIdAndProp, + reduce((o, cb) => concat(o, cb.callback.outputs), [], 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)], + cb.callback.inputs + ), + candidates + ); +} + +export const getLayoutCallbacks = ( + graphs: any, + paths: any, + layout: any, + options: any +): ICallback[] => getReadyCallbacks(getCallbacksInLayout( + graphs, + paths, + layout, + options +)); + export const getUniqueIdentifier = ({ anyVals, callback: { diff --git a/dash-renderer/src/actions/index.js b/dash-renderer/src/actions/index.js index 226f0bcc0a..7d165f41a6 100644 --- a/dash-renderer/src/actions/index.js +++ b/dash-renderer/src/actions/index.js @@ -4,8 +4,8 @@ import {addRequestedCallbacks} from './callbacks'; import {getAppState} from '../reducers/constants'; import {getAction} from './constants'; import cookie from 'cookie'; -import {getCallbacksInLayout, validateCallbacksToLayout} from './dependencies'; -import {includeObservers} from './dependencies_ts'; +import {validateCallbacksToLayout} from './dependencies'; +import {includeObservers, getLayoutCallbacks} from './dependencies_ts'; import {getPath} from './paths'; export const onError = createAction(getAction('ON_ERROR')); @@ -66,11 +66,13 @@ function triggerDefaultState(dispatch, getState) { ); } - const initialCallbacks = getCallbacksInLayout(graphs, paths, layout, { - outputsOnly: true, - }); - - dispatch(addRequestedCallbacks(initialCallbacks)); + dispatch( + addRequestedCallbacks( + getLayoutCallbacks(graphs, paths, layout, { + outputsOnly: true, + }) + ) + ); } export const redo = moveHistory('REDO'); diff --git a/tests/integration/callbacks/test_missing_inputs.py b/tests/integration/callbacks/test_missing_inputs.py index 28b5fec7bd..2cfffb8ecc 100644 --- a/tests/integration/callbacks/test_missing_inputs.py +++ b/tests/integration/callbacks/test_missing_inputs.py @@ -62,15 +62,14 @@ def out3(out1, title): dash_duo.start_server(app) wait_for_queue(dash_duo) - # out3 fires because it has an existing input + # out3 fires because it has another Input besides out1 dash_duo.wait_for_text_to_equal("#out3", "output1 init - 3 - Title") assert dash_duo.find_element("#out1").text == "output1 init" - # out2 is fired because while its only input (out1) is "prevented", - # it exists and provides a value for it to execute + # out2 doesn't fire because its only input (out1) is "prevented" # State items don't matter for this. - assert dash_duo.find_element("#out2").text == "output1 init - 2 - Title" + assert dash_duo.find_element("#out2").text == "output2 init" dash_duo.find_element("#btn").click() @@ -115,9 +114,9 @@ def out3(out1, out2): dash_duo.start_server(app) wait_for_queue(dash_duo) - assert dash_duo.find_element("#out1").text == "output1 init" - assert dash_duo.find_element("#out2").text == "output2 init" - assert dash_duo.find_element("#out3").text == "output1 initoutput2 init" + + for i in ["1", "2", "3"]: + assert dash_duo.find_element("#out" + i).text == "output{} init".format(i) dash_duo.find_element("#btn").click() # now all callbacks fire @@ -384,7 +383,7 @@ def out2(out1): dash_duo.wait_for_text_to_equal("#content", "content init") assert dash_duo.find_element("#out1").text == "output1 init" - assert dash_duo.find_element("#out2").text == "output1 init - 2" + assert dash_duo.find_element("#out2").text == "output2 init" dash_duo.find_element("#btn").click() dash_duo.wait_for_text_to_equal("#out1", "A - item 0") @@ -473,7 +472,7 @@ def cssid(v): # out2 doesn't fire because its only input (out1) is "prevented" # State items don't matter for this. - assert dash_duo.find_element(cssid("out2")).text == "output1 init - 2 - Title" + assert dash_duo.find_element(cssid("out2")).text == "output2 init" dash_duo.find_element("#btn").click() diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index f5a5645d16..a033a7d8aa 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -183,9 +183,9 @@ def update_slider2_label(val): return "Slider2 value {}".format(val) dash_duo.start_server(app) - - dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 0") - dash_duo.wait_for_text_to_equal("#label2", "Slider2 value 0") + time.sleep(10000) + dash_duo.wait_for_text_to_equal("#label1", "") + dash_duo.wait_for_text_to_equal("#label2", "") dash_duo.find_element("#button").click() dash_duo.wait_for_text_to_equal("#label1", "Slider1 value 1") @@ -226,9 +226,6 @@ def update_sliders(button1, button2): [Input("slider1", "value"), Input("slider2", "value")], ) def update_graph(s1, s2): - if s1 is None or s2 is None: - raise PreventUpdate - return "x={}, y={}".format(s1, s2) dash_duo.start_server(app) diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 71f4874aba..8e3bbe11c1 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -1,4 +1,5 @@ from multiprocessing import Value + import dash from dash.dependencies import Input, Output from dash.exceptions import PreventUpdate diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 427aa04b67..8476f9fb90 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -156,8 +156,8 @@ def callback2(value): ) assert ( - callback2_count.value == 1 - ), "callback2 is triggered on initial load, even if callback1 is prevented" + callback2_count.value == 0 + ), "callback2 is never triggered, even on initial load" # double check that output1 and output2 children were not updated assert dash_duo.find_element("#output1").text == initial_output From f7d36786f91d5f4980287bc248cc2af34acf901f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 16:33:38 -0400 Subject: [PATCH 28/90] remove timeout --- tests/integration/callbacks/test_multiple_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_multiple_callbacks.py b/tests/integration/callbacks/test_multiple_callbacks.py index a033a7d8aa..d9b13503a0 100644 --- a/tests/integration/callbacks/test_multiple_callbacks.py +++ b/tests/integration/callbacks/test_multiple_callbacks.py @@ -183,7 +183,7 @@ def update_slider2_label(val): return "Slider2 value {}".format(val) dash_duo.start_server(app) - time.sleep(10000) + dash_duo.wait_for_text_to_equal("#label1", "") dash_duo.wait_for_text_to_equal("#label2", "") From 77528d1e8f29ecb8a55f198ce12da7e6e58ce5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 May 2020 17:57:54 -0400 Subject: [PATCH 29/90] on layout change: ignore callbacks for which inputs were excluded or missing --- dash-renderer/src/actions/dependencies_ts.ts | 55 +++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 90e6194369..1ca962bf99 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -1,10 +1,13 @@ import { all, + any, assoc, concat, + difference, filter, flatten, forEach, + isEmpty, keys, map, mergeWith, @@ -13,7 +16,7 @@ import { reduce } from 'ramda'; import { ICallback, ICallbackProperty } from '../types/callbacks'; -import { getCallbacksByInput, splitIdAndProp, stringifyId, getCallbacksInLayout } from './dependencies'; +import { getCallbacksByInput, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued } from './dependencies'; import { getPath } from './paths'; export const DIRECT = 2; @@ -54,12 +57,50 @@ export const getLayoutCallbacks = ( paths: any, layout: any, options: any -): ICallback[] => getReadyCallbacks(getCallbacksInLayout( - graphs, - paths, - layout, - options -)); +): ICallback[] => { + let exclusions: string[] = []; + const initial = getCallbacksInLayout( + graphs, + paths, + layout, + options + ); + let callbacks = initial; + + while (true) { + // Find callbacks for which all inputs are missing or in the exclusions + const [included, excluded] = partition(({ + callback: { inputs }, + getInputs + }) => any(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 + ))) + ); + } + + // only return ready callbacks (the executed callbacks will ensure followups are triggered) + return getReadyCallbacks(callbacks); + +} export const getUniqueIdentifier = ({ anyVals, From 22e1e236a8c3e9b94d4e7e339db2336bfea5daa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 19 May 2020 20:05:15 -0400 Subject: [PATCH 30/90] fix test regressions --- dash-renderer/src/AppProvider.react.tsx | 237 ++++++++++++++++--- dash-renderer/src/actions/callbacks.ts | 8 +- dash-renderer/src/actions/dependencies_ts.ts | 74 +++++- dash-renderer/src/reducers/callbacks.ts | 11 +- dash-renderer/src/types/callbacks.ts | 8 + 5 files changed, 293 insertions(+), 45 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 43873424e1..391b6fad38 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -1,13 +1,16 @@ import { + all, assoc, concat, difference, + filter, find, flatten, forEach, groupBy, has, includes, + intersection, isEmpty, isNil, keys, @@ -17,6 +20,7 @@ import { pickBy, pluck, reduce, + toPairs, uniq, values } from 'ramda'; @@ -29,9 +33,9 @@ import AppContainer from './AppContainer.react'; import PropTypes from 'prop-types'; import { - updateProps, + handleAsyncError, setPaths, - handleAsyncError + updateProps } from './actions'; import { addCompletedCallbacks, @@ -39,6 +43,7 @@ import { addExecutingCallbacks, addPrioritizedCallbacks, addRequestedCallbacks, + addStoredCallbacks, addWatchedCallbacks, aggregateCallbacks, executeCallback, @@ -46,17 +51,38 @@ import { removeExecutingCallbacks, removePrioritizedCallbacks, removeRequestedCallbacks, + removeStoredCallbacks, removeWatchedCallbacks, setPendingCallbacks } from './actions/callbacks'; import { getPath, computePaths } from './actions/paths'; -import { stringifyId, parseIfWildcard, getCallbacksByInput } from './actions/dependencies'; -import { combineIdAndProp, getUniqueIdentifier, includeObservers, pruneCallbacks, getReadyCallbacks, getLayoutCallbacks } from './actions/dependencies_ts'; +import { + getCallbacksByInput, + parseIfWildcard, + stringifyId, + isMultiValued +} from './actions/dependencies'; +import { + combineIdAndProp, + getLayoutCallbacks, + getReadyCallbacks, + getUniqueIdentifier, + includeObservers, + pruneCallbacks +} from './actions/dependencies_ts'; import { ICallbacksState } from './reducers/callbacks'; -import { IExecutingCallback, ICallback, ICallbackProperty } from './types/callbacks'; +import { + IExecutingCallback, + ICallback, + ICallbackProperty, + IStoredCallback +} from './types/callbacks'; import isAppReady from './actions/isAppReady'; -import { prunePersistence, applyPersistence } from './persistence'; +import { + applyPersistence, + prunePersistence +} from './persistence'; const store = initializeStore(); @@ -78,7 +104,6 @@ observe(({ } = getState(); const next = getPendingCallbacks(callbacks); - console.log('onCallbacksChanged', '[pendingCallbacks-candidate]', callbacks, next); /** * If the calculated list of pending callbacks is equivalent @@ -94,7 +119,6 @@ observe(({ return; } - console.log('onCallbacksChanged', '[pendingCallbacks]', next); dispatch(setPendingCallbacks(next)); }, ['callbacks']); @@ -102,12 +126,12 @@ observe(({ dispatch, getState }) => { - const { callbacks, callbacks: { prioritized, executing, watched, executed, completed }, paths } = getState(); + const { callbacks, callbacks: { prioritized, executing, watched, executed, completed, stored }, paths } = getState(); let { callbacks: { requested } } = getState(); const pendingCallbacks = getPendingCallbacks(callbacks); - console.log('onCallbacksChanged.requested', completed, requested); + console.log('onCallbacksChanged.requested', requested, completed, callbacks); /* 1. Remove duplicated `requested` callbacks @@ -118,7 +142,7 @@ observe(({ these callbacks are duplicates. */ const rDuplicates = flatten(map( - group => group.slice(1), + group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -128,6 +152,7 @@ observe(({ )); /* + TODO? Clean up the `requested` list - during the dispatch phase, duplicates will be removed for real */ @@ -142,7 +167,7 @@ observe(({ these callbacks are `prioritized` and duplicates. */ const pDuplicates = flatten(map( - group => group.slice(1), + group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -152,7 +177,7 @@ observe(({ )); const eDuplicates = flatten(map( - group => group.slice(1), + group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -162,7 +187,7 @@ observe(({ )) as IExecutingCallback[]; const wDuplicates = flatten(map( - group => group.slice(1), + group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -189,6 +214,7 @@ observe(({ } /* + TODO? Clean up the `requested` list - during the dispatch phase, it will be updated for real */ @@ -203,7 +229,8 @@ observe(({ /* 4. Find `requested` callbacks that do not depend on a outstanding output (as either input or state) */ - const readyCallbacks = getReadyCallbacks(requested, pendingCallbacks); + let readyCallbacks = getReadyCallbacks(requested, pendingCallbacks); + console.log('onCallbacksChanged.requested', '[readyCallbacks]', readyCallbacks); /* If: @@ -223,6 +250,73 @@ observe(({ requested.length ) ? requested : []; + /* + 5. Prune callbacks that became irrelevant in their `executionGroup` + */ + const pendingGroups = groupBy( + cb => cb.executionGroup as any, + filter(cb => !isNil(cb.executionGroup), stored) + ); + console.log('onCallbacksChanged.requested', '[pendingGroups]', pendingGroups, map(pg => flatten(map( + gcb => gcb.executionMeta.updatedProps, + pg + )), values(pendingGroups))); + + const dropped: ICallback[] = filter(cb => { + if (!cb.executionGroup || !pendingGroups[cb.executionGroup] || !pendingGroups[cb.executionGroup].length) { + return false; + } + + const inputs = map(combineIdAndProp, flatten(cb.getInputs(paths))); + + const allProps = flatten(map( + gcb => gcb.executionMeta.allProps, + pendingGroups[cb.executionGroup] + )); + + const updated = flatten(map( + gcb => gcb.executionMeta.updatedProps, + pendingGroups[cb.executionGroup] + )); + + const res = + isEmpty(intersection( + inputs, + updated + )) && + isEmpty(difference( + inputs, + allProps + )) + && !all( + isMultiValued, + cb.callback.inputs + ); + + console.log('SPECIAL', cb, res, inputs, allProps, updated); + + return res; + }, + readyCallbacks + ); + + console.log('onCallbacksChanged.requested', '[dropped]', readyCallbacks, dropped, pendingGroups); + + /* + 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, @@ -240,6 +334,8 @@ observe(({ wAdded.length ? addWatchedCallbacks(wAdded) : null, // Prune circular callbacks rCircular.length ? removeRequestedCallbacks(rCircular) : null, + // Drop non-triggered initial callbacks + dropped.length ? removeRequestedCallbacks(dropped) : null, // Promote callbacks readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null @@ -298,15 +394,11 @@ observe(async ({ Make sure to only execute callbacks that are still in the `prioritized` list (isAppReady is async - state could have changed) */ const { callbacks: { prioritized: updatedPrioritized } } = getState(); - const [remainingCallbacks, droppedCallbacks] = partition( + const [remainingCallbacks] = partition( ([cb]) => includes(cb, updatedPrioritized), callbacks ); - if (droppedCallbacks.length) { - console.log('onCallbacksChanged.prioritized', '[dropped]', map(([cb]) => cb, droppedCallbacks)); - } - const executingCallbacks: IExecutingCallback[] = remainingCallbacks.map(([cb, stash]) => { return executeCallback(cb, config, hooks, paths, layout, stash); }); @@ -410,30 +502,36 @@ observe(({ console.log('onCallbacksChanged.executed', executed); - let callbacks: ICallback[] = []; - forEach(({ executionResult }) => { + let requestedCallbacks: ICallback[] = []; + let storedCallbacks: IStoredCallback[] = []; + + forEach(cb => { + const { executionResult } = cb; + if (isNil(executionResult)) { return; } const { data, error } = executionResult; - console.log('SPECIAL', '[executionResult]', data); + console.log('onCallbacksChanged.executed', '[executionResult]', cb, data); if (data !== undefined) { - return forEach(([id, props]: [any, { [key: string]: any }]) => { + 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); - callbacks = concat( - callbacks, + // Skip prop-triggered callbacks for callbacks with an execution group - these callbacks + // should already be present in `requested` + requestedCallbacks = concat( + requestedCallbacks, flatten(map( prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop), keys(props) )) - ) + ); // New layout - trigger callbacks for that explicitly if (has('children', appliedProps)) { @@ -445,8 +543,8 @@ observe(({ const paths = computePaths(children, oldChildrenPath, oldPaths); dispatch(setPaths(paths)); - callbacks = concat( - callbacks, + requestedCallbacks = concat( + requestedCallbacks, getLayoutCallbacks(graphs, paths, children, { chunkPath: oldChildrenPath, }) @@ -454,8 +552,8 @@ observe(({ // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger // even due to the deletion of components - callbacks = concat( - callbacks, + requestedCallbacks = concat( + requestedCallbacks, getLayoutCallbacks(graphs, oldPaths, oldChildren, { removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath }) @@ -472,27 +570,96 @@ observe(({ if (!isEmpty(addedProps)) { const { graphs, paths } = getState(); - callbacks = concat( - callbacks, + requestedCallbacks = concat( + requestedCallbacks, includeObservers(id, addedProps, graphs, paths) ); } - }, Object.entries(data)); + + + + 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) { handleAsyncError(error, error.message, dispatch); + + storedCallbacks.push({ + ...cb, + executionMeta: { + allProps: map(combineIdAndProp, flatten(cb.getOutputs(getState().paths))), + updatedProps: [] + } + }); } }, executed); + console.log('SPECIAL', '[requestedCallbacks]', requestedCallbacks); dispatch(aggregateCallbacks([ executed.length ? removeExecutedCallbacks(executed) : null, executed.length ? addCompletedCallbacks(executed.length) : null, - callbacks.length ? addRequestedCallbacks(callbacks) : null + storedCallbacks.length ? addStoredCallbacks(storedCallbacks) : null, + requestedCallbacks.length ? addRequestedCallbacks(requestedCallbacks) : null ])); }, ['callbacks.executed']); +observe(({ + dispatch, + getState +}) => { + const { callbacks } = getState(); + const pendingCallbacks = getPendingCallbacks(callbacks); + + let { callbacks: { stored } } = getState(); + + console.log('onCallbacksChanged.stored', stored); + + 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, + callbacks + ]) => !pendingGroups[executionGroup] ? + concat(res, callbacks) : + res, + [] as IStoredCallback[], + toPairs(executionGroups) + ); + + console.log('onCallbacksChanged.stored', '[dropped]', nullGroupCallbacks, dropped); + + dispatch(aggregateCallbacks([ + nullGroupCallbacks.length ? removeStoredCallbacks(nullGroupCallbacks) : null, + dropped.length ? removeStoredCallbacks(dropped): null + ])); +}, ['callbacks.stored', 'callbacks.completed']) + const AppProvider = ({hooks}: any) => { return ( diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 3df508bbdc..9732602199 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -12,7 +12,7 @@ import { import { STATUS } from '../constants/constants'; import { CallbackActionType, CallbackAggregateActionType } from "../reducers/callbacks"; -import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback } from '../types/callbacks'; +import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, IStoredCallback } from '../types/callbacks'; import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies'; import { urlBase } from './utils'; import { getCSRFHeader } from '.'; @@ -35,6 +35,9 @@ export const addPrioritizedCallbacks = createAction( export const addRequestedCallbacks = createAction( CallbackActionType.AddRequested ); +export const addStoredCallbacks = createAction( + CallbackActionType.AddStored +); export const addWatchedCallbacks = createAction(CallbackActionType.AddWatched); export const removeExecutedCallbacks = createAction( CallbackActionType.RemoveExecuted @@ -48,6 +51,9 @@ export const removePrioritizedCallbacks = createAction( export const removeRequestedCallbacks = createAction( CallbackActionType.RemoveRequested ); +export const removeStoredCallbacks = createAction( + CallbackActionType.RemoveStored +); export const removeWatchedCallbacks = createAction( CallbackActionType.RemoveWatched ); diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 1ca962bf99..ca22926fce 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -1,6 +1,5 @@ import { all, - any, assoc, concat, difference, @@ -28,6 +27,41 @@ export const combineIdAndProp = ({ property }: ICallbackProperty) => `${stringifyId(id)}.${property}`; +// /* +// * 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 const followForward = ( +// graphs: any, +// paths: any, +// callbacks: ICallback[] +// ): ICallback[] => { +// callbacks = callbacks.slice(0); +// let i; +// let callback: ICallback; + +// const followOutput = ({ id, property }: ICallbackProperty) => { +// callbacks = concat(callbacks, getCallbacksByInput( +// graphs, +// paths, +// id, +// property, +// INDIRECT +// )); +// }; + +// // 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; +// } + export const getReadyCallbacks = ( candidates: ICallback[], callbacks: ICallback[] = candidates @@ -59,20 +93,39 @@ export const getLayoutCallbacks = ( options: any ): ICallback[] => { let exclusions: string[] = []; - const initial = getCallbacksInLayout( + let callbacks = getCallbacksInLayout( graphs, paths, layout, options ); - let callbacks = initial; - + console.log('SPECIAL', '[getLayoutCallbacks-initial]', callbacks); + + // /* + // Basic implementation - retrieve all `ready` callbacks. + // Follow up callbacks will be triggered by executed callbacks. + // */ + // return getReadyCallbacks(callbacks); + + /* + This loop is for backward compatibility with previous implementation + of the callbacks chain. 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 - }) => any(isMultiValued, inputs) || + }) => all(isMultiValued, inputs) || !isEmpty(difference( map(combineIdAndProp, flatten(getInputs(paths))), exclusions @@ -97,9 +150,14 @@ export const getLayoutCallbacks = ( ); } - // only return ready callbacks (the executed callbacks will ensure followups are triggered) - return getReadyCallbacks(callbacks); - + /* + 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 = ({ diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 820d2f1e3b..5007c93add 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -7,7 +7,8 @@ import { import { ICallback, IExecutedCallback, - IExecutingCallback + IExecutingCallback, + IStoredCallback } from '../types/callbacks'; export enum CallbackActionType { @@ -16,12 +17,14 @@ export enum CallbackActionType { AddExecuting = 'Callbacks.AddExecuting', AddPrioritized = 'Callbacks.AddPrioritized', AddRequested = 'Callbacks.AddRequested', + AddStored = 'Callbacks.AddStored', AddWatched = 'Callbacks.AddWatched', RemoveApplied = 'Callbacks.RemoveApplied', RemoveExecuted = 'Callbacks.RemoveExecuted', RemoveExecuting = 'Callbacks.RemoveExecuting', RemovePrioritized = 'Callbacks.ReomvePrioritized', RemoveRequested = 'Callbacks.RemoveRequested', + RemoveStored = 'Callbacks.RemoveStored', RemoveWatched = 'Callbacks.RemoveWatched' } @@ -56,6 +59,7 @@ export interface ICallbacksState { executing: IExecutingCallback[]; watched: IExecutingCallback[]; executed: IExecutedCallback[]; + stored: IStoredCallback[]; completed: number; } @@ -64,6 +68,7 @@ const DEFAULT_STATE: ICallbacksState = { executing: [], prioritized: [], requested: [], + stored: [], watched: [], completed: 0 }; @@ -76,12 +81,14 @@ const transforms: { [CallbackActionType.AddExecuting]: concat, [CallbackActionType.AddPrioritized]: concat, [CallbackActionType.AddRequested]: concat, + [CallbackActionType.AddStored]: concat, [CallbackActionType.AddWatched]: concat, [CallbackActionType.RemoveApplied]: difference, [CallbackActionType.RemoveExecuted]: difference, [CallbackActionType.RemoveExecuting]: difference, [CallbackActionType.RemovePrioritized]: difference, [CallbackActionType.RemoveRequested]: difference, + [CallbackActionType.RemoveStored]: difference, [CallbackActionType.RemoveWatched]: difference, }; @@ -92,11 +99,13 @@ const fields: { [CallbackActionType.AddExecuting]: 'executing', [CallbackActionType.AddPrioritized]: 'prioritized', [CallbackActionType.AddRequested]: 'requested', + [CallbackActionType.AddStored]: 'stored', [CallbackActionType.AddWatched]: 'watched', [CallbackActionType.RemoveExecuted]: 'executed', [CallbackActionType.RemoveExecuting]: 'executing', [CallbackActionType.RemovePrioritized]: 'prioritized', [CallbackActionType.RemoveRequested]: 'requested', + [CallbackActionType.RemoveStored]: 'stored', [CallbackActionType.RemoveWatched]: 'watched' } diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 5a1d153818..a614e332ed 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -15,6 +15,7 @@ export interface ICallback { outputs: ICallbackProperty[]; state: ICallbackProperty[]; }; + executionGroup?: string; getInputs: (paths: any) => ICallbackProperty[]; getOutputs: (paths: any) => ICallbackProperty[]; getState: (paths: any) => ICallbackProperty[]; @@ -31,6 +32,13 @@ export interface IExecutedCallback extends IExecutingCallback { executionResult: CallbackResult | null; } +export interface IStoredCallback extends IExecutedCallback { + executionMeta: { + allProps: string[]; + updatedProps: string[]; + } +} + export type CallbackResult = { data?: any; error?: Error From d14c7823b248629af733d10eff920ac8812508f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 19 May 2020 20:58:08 -0400 Subject: [PATCH 31/90] drop tail, not head of duplicates --- dash-renderer/src/AppProvider.react.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 391b6fad38..7002a30c35 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -142,7 +142,8 @@ observe(({ these callbacks are duplicates. */ const rDuplicates = flatten(map( - group => filter(cb => !cb.executionGroup, group).slice(1), + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -167,31 +168,34 @@ observe(({ these callbacks are `prioritized` and duplicates. */ const pDuplicates = flatten(map( - group => filter(cb => !cb.executionGroup, group).slice(1), + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, - concat(requested, prioritized) + concat(prioritized, requested) ) ) )); const eDuplicates = flatten(map( - group => filter(cb => !cb.executionGroup, group).slice(1), + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, - concat(requested, executing) + concat(executing, requested) ) ) )) as IExecutingCallback[]; const wDuplicates = flatten(map( - group => filter(cb => !cb.executionGroup, group).slice(1), + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, - concat(requested, watched) + concat(watched, requested) ) ) )) as IExecutingCallback[]; From b021f389633a0add01c8d20dff89f9c1112be2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 19 May 2020 21:03:12 -0400 Subject: [PATCH 32/90] unrelated fix for cbcx001 (the space character is a control character for cookies?) --- tests/integration/callbacks/test_callback_context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"' From 5656e180fdb7681f2af44f6c47b45e580f968027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 19 May 2020 21:14:21 -0400 Subject: [PATCH 33/90] add ts-jest/babel for jest tests --- dash-renderer/jest.config.js | 2 +- dash-renderer/package-lock.json | 100 ++++++++++++++++++++++++++++++++ dash-renderer/package.json | 1 + 3 files changed, 102 insertions(+), 1 deletion(-) 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 47af7a2c44..e70fb6e82f 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -4656,6 +4656,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", @@ -12126,6 +12135,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", @@ -18383,6 +18398,91 @@ "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", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index f0c8842805..11df48682d 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -68,6 +68,7 @@ "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", "typescript": "^3.8.3", "webpack": "^4.42.0", From 285c15255c4e9857de8b87e5884fc9c7e9226ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 06:43:14 -0400 Subject: [PATCH 34/90] fix regression in error handling / display --- dash-renderer/src/AppProvider.react.tsx | 21 ++++++++++++++++++--- dash-renderer/src/actions/callbacks.ts | 12 ++++++------ dash-renderer/src/types/callbacks.ts | 15 +++++++++++++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 7002a30c35..b1161fd8a6 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -510,13 +510,19 @@ observe(({ let storedCallbacks: IStoredCallback[] = []; forEach(cb => { - const { executionResult } = cb; + const { + callback: { + clientside_function, + output + }, + executionResult + } = cb; if (isNil(executionResult)) { return; } - const { data, error } = executionResult; + const { data, error, payload } = executionResult; console.log('onCallbacksChanged.executed', '[executionResult]', cb, data); if (data !== undefined) { @@ -599,7 +605,16 @@ observe(({ } if (error !== undefined) { - handleAsyncError(error, error.message, dispatch); + 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, diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 9732602199..73b0c8867a 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -243,7 +243,7 @@ export function executeCallback( payload.state = fillVals(paths, layout, cb, state, 'State'); } } catch (error) { - resolve({ error }); + resolve({ error, payload }); } function handleClientside(clientside_function: any, payload: any) { @@ -339,15 +339,15 @@ export function executeCallback( if (clientside_function) { try { - resolve({ data: handleClientside(clientside_function, payload) }); + resolve({ data: handleClientside(clientside_function, payload), payload }); } catch (error) { - resolve({ error }); + resolve({ error, payload }); } return null; } else { handleServerside(payload) - .then(data => resolve({ data })) - .catch(error => resolve({ error })); + .then(data => resolve({ data, payload })) + .catch(error => resolve({ error, payload })); } }); @@ -360,7 +360,7 @@ export function executeCallback( } catch (error) { return { ...cb, - executionPromise: { error } + executionPromise: { error, payload: null } }; } } \ No newline at end of file diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index a614e332ed..0ac9b7a125 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -8,7 +8,10 @@ export interface ICallbackProperty { export interface ICallback { anyVals: any[] | string; callback: { - clientside_function: string; + clientside_function?: { + namespace: string; + function_name: string; + }; input: string; inputs: ICallbackProperty[]; output: string; @@ -39,7 +42,15 @@ export interface IStoredCallback extends IExecutedCallback { } } +interface ICallbackPayload { + changedPropIds: any[]; + inputs: any[]; + output: string; + outputs: any[]; +} + export type CallbackResult = { data?: any; - error?: Error + error?: Error; + payload: ICallbackPayload | null; } \ No newline at end of file From bf96924eb12f08df6f5fde484600a21599cb02a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 08:15:48 -0400 Subject: [PATCH 35/90] post-merge: add back callback context for clientside callbacks --- dash-renderer/src/actions/callbacks.ts | 83 +++++++++++++++++++------- dash-renderer/src/types/callbacks.ts | 3 +- dash/testing/application_runners.py | 14 +++-- dash/testing/plugin.py | 7 ++- 4 files changed, 79 insertions(+), 28 deletions(-) diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 73b0c8867a..3392a2a727 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -12,7 +12,7 @@ import { import { STATUS } from '../constants/constants'; import { CallbackActionType, CallbackAggregateActionType } from "../reducers/callbacks"; -import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, IStoredCallback } from '../types/callbacks'; +import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, ICallbackPayload, IStoredCallback } from '../types/callbacks'; import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies'; import { urlBase } from './utils'; import { getCSRFHeader } from '.'; @@ -231,22 +231,63 @@ export function executeCallback( } const __promise = new Promise(resolve => { - let payload: any; try { - payload = { + 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 (cb.callback.state.length) { - payload.state = fillVals(paths, layout, cb, state, 'State'); + + if (clientside_function) { + try { + resolve({ data: handleClientside(clientside_function, payload), payload }); + } catch (error) { + resolve({ error, payload }); + } + return null; + } else { + handleServerside(payload) + .then(data => resolve({ data, payload })) + .catch(error => resolve({ error, payload })); } } catch (error) { - resolve({ error, payload }); + resolve({ error, payload: null }); + } + + 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; } - function handleClientside(clientside_function: any, payload: any) { + 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', { @@ -270,12 +311,27 @@ export function executeCallback( 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') { @@ -336,19 +392,6 @@ export function executeCallback( throw res; }); } - - if (clientside_function) { - try { - resolve({ data: handleClientside(clientside_function, payload), payload }); - } catch (error) { - resolve({ error, payload }); - } - return null; - } else { - handleServerside(payload) - .then(data => resolve({ data, payload })) - .catch(error => resolve({ error, payload })); - } }); const newCb = { diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 0ac9b7a125..407c8c4078 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -42,11 +42,12 @@ export interface IStoredCallback extends IExecutedCallback { } } -interface ICallbackPayload { +export interface ICallbackPayload { changedPropIds: any[]; inputs: any[]; output: string; outputs: any[]; + state?: any[] | null; } export type CallbackResult = { diff --git a/dash/testing/application_runners.py b/dash/testing/application_runners.py index 75b1dc0bb3..d395792664 100644 --- a/dash/testing/application_runners.py +++ b/dash/testing/application_runners.py @@ -222,8 +222,7 @@ def stop(self): logger.info("proc.terminate with pid %s", self.proc.pid) self.proc.terminate() if self.tmp_app_path and os.path.exists(self.tmp_app_path): - logger.debug("removing temporary app path %s", - self.tmp_app_path) + logger.debug("removing temporary app path %s", self.tmp_app_path) shutil.rmtree(self.tmp_app_path) if utils.PY3: # pylint:disable=no-member @@ -343,7 +342,9 @@ def start(self, app, start_timeout=2, cwd=None): class JuliaRunner(ProcessRunner): def __init__(self, keep_open=False, stop_timeout=3): - super(JuliaRunner, self).__init__(keep_open=keep_open, stop_timeout=stop_timeout) + super(JuliaRunner, self).__init__( + keep_open=keep_open, stop_timeout=stop_timeout + ) self.proc = None # pylint: disable=arguments-differ @@ -385,7 +386,9 @@ def start(self, app, start_timeout=30, cwd=None): logger.warning("get cwd from inspect => %s", cwd) break if cwd: - logger.info("JuliaRunner inferred cwd from the Python call stack: %s", cwd) + logger.info( + "JuliaRunner inferred cwd from the Python call stack: %s", cwd + ) # try copying all valid sub folders (i.e. assets) in cwd to tmp # note that the R assets folder name can be any valid folder name @@ -415,8 +418,7 @@ def start(self, app, start_timeout=30, cwd=None): logger.info("Run Dash.jl app with julia => %s", app) args = shlex.split( - "julia {}".format(os.path.realpath(app)), - posix=not self.is_windows, + "julia {}".format(os.path.realpath(app)), posix=not self.is_windows, ) logger.debug("start Dash.jl process with %s", args) diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 60e774fcbc..0f2a8d313b 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -4,7 +4,12 @@ try: - from dash.testing.application_runners import ThreadedRunner, ProcessRunner, RRunner, JuliaRunner + from dash.testing.application_runners import ( + ThreadedRunner, + ProcessRunner, + RRunner, + JuliaRunner, + ) from dash.testing.browser import Browser from dash.testing.composite import DashComposite, DashRComposite, DashJuliaComposite except ImportError: From 7ab1d3f7139912eb62f1215dd0ea1a867e4cb06b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 08:21:52 -0400 Subject: [PATCH 36/90] post-merge: fix server status in devtools --- dash-renderer/src/actions/callbacks.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 3392a2a727..a9aa668ad2 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -390,6 +390,11 @@ export function executeCallback( 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.'); }); } }); From 4b99a0820afcc61d2fa64a8544583a132347b0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 12:02:10 -0400 Subject: [PATCH 37/90] refactor observers into files --- dash-renderer/src/AppProvider.react.tsx | 685 +----------------- dash-renderer/src/StoreObserver.ts | 43 +- .../src/observers/executedCallbacks.ts | 222 ++++++ .../src/observers/executingCallbacks.ts | 73 ++ .../src/observers/pendingCallbacks.ts | 36 + .../src/observers/prioritizedCallbacks.ts | 106 +++ .../src/observers/requestedCallbacks.ts | 274 +++++++ .../src/observers/storedCallbacks.ts | 73 ++ dash-renderer/src/store.ts | 17 +- dash-renderer/src/utils/callbacks.ts | 15 + 10 files changed, 850 insertions(+), 694 deletions(-) create mode 100644 dash-renderer/src/observers/executedCallbacks.ts create mode 100644 dash-renderer/src/observers/executingCallbacks.ts create mode 100644 dash-renderer/src/observers/pendingCallbacks.ts create mode 100644 dash-renderer/src/observers/prioritizedCallbacks.ts create mode 100644 dash-renderer/src/observers/requestedCallbacks.ts create mode 100644 dash-renderer/src/observers/storedCallbacks.ts create mode 100644 dash-renderer/src/utils/callbacks.ts diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index b1161fd8a6..897bfa6493 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -1,683 +1,24 @@ -import { - all, - assoc, - concat, - difference, - filter, - find, - flatten, - forEach, - groupBy, - has, - includes, - intersection, - isEmpty, - isNil, - keys, - map, - partition, - path, - pickBy, - pluck, - reduce, - toPairs, - uniq, - values -} from 'ramda'; - +import PropTypes from 'prop-types'; import React from 'react'; import {Provider} from 'react-redux'; import initializeStore, { observe } from './store'; import AppContainer from './AppContainer.react'; -import PropTypes from 'prop-types'; -import { - handleAsyncError, - setPaths, - updateProps -} from './actions'; -import { - addCompletedCallbacks, - addExecutedCallbacks, - addExecutingCallbacks, - addPrioritizedCallbacks, - addRequestedCallbacks, - addStoredCallbacks, - addWatchedCallbacks, - aggregateCallbacks, - executeCallback, - removeExecutedCallbacks, - removeExecutingCallbacks, - removePrioritizedCallbacks, - removeRequestedCallbacks, - removeStoredCallbacks, - removeWatchedCallbacks, - setPendingCallbacks -} from './actions/callbacks'; -import { getPath, computePaths } from './actions/paths'; - -import { - getCallbacksByInput, - parseIfWildcard, - stringifyId, - isMultiValued -} from './actions/dependencies'; -import { - combineIdAndProp, - getLayoutCallbacks, - getReadyCallbacks, - getUniqueIdentifier, - includeObservers, - pruneCallbacks -} from './actions/dependencies_ts'; -import { ICallbacksState } from './reducers/callbacks'; -import { - IExecutingCallback, - ICallback, - ICallbackProperty, - IStoredCallback -} from './types/callbacks'; -import isAppReady from './actions/isAppReady'; -import { - applyPersistence, - prunePersistence -} from './persistence'; +import pendingCallbacks from './observers/pendingCallbacks'; +import requestedCallbacks from './observers/requestedCallbacks'; +import prioritizeCallbacks from './observers/prioritizedCallbacks'; +import executingCallbacks from './observers/executingCallbacks'; +import executedCallbacks from './observers/executedCallbacks'; +import storedCallbacks from './observers/storedCallbacks'; const store = initializeStore(); - -const getPendingCallbacks = ({ executed, executing, prioritized, requested, watched }: ICallbacksState) => [ - ...requested, - ...prioritized, - ...executing, - ...watched, - ...executed -]; - -observe(({ - dispatch, - getState -}) => { - const { - callbacks, - pendingCallbacks - } = getState(); - - const next = getPendingCallbacks(callbacks); - - /** - * If the calculated list of pending callbacks is equivalent - * to the previous one, do not update it. - */ - if ( - pendingCallbacks && - pendingCallbacks.length === next.length && - next.every((v, i) => - v === pendingCallbacks[i] || - v.callback === pendingCallbacks[i].callback) - ) { - return; - } - - dispatch(setPendingCallbacks(next)); -}, ['callbacks']); - -observe(({ - dispatch, - getState -}) => { - const { callbacks, callbacks: { prioritized, executing, watched, executed, completed, stored }, paths } = getState(); - let { callbacks: { requested } } = getState(); - - const pendingCallbacks = getPendingCallbacks(callbacks); - - console.log('onCallbacksChanged.requested', requested, completed, callbacks); - - /* - 1. Remove duplicated `requested` callbacks - */ - - /* - Extract all but the first callback from each IOS-key group - these callbacks are duplicates. - */ - const rDuplicates = flatten(map( - group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(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), - // group => filter(cb => !cb.executionGroup, group).slice(1), - values( - groupBy( - getUniqueIdentifier, - concat(prioritized, requested) - ) - ) - )); - - const eDuplicates = flatten(map( - group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), - values( - groupBy( - getUniqueIdentifier, - concat(executing, requested) - ) - ) - )) as IExecutingCallback[]; - - const wDuplicates = flatten(map( - group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), - values( - groupBy( - getUniqueIdentifier, - concat(watched, requested) - ) - ) - )) as IExecutingCallback[]; - - if (rDuplicates.length || pDuplicates.length || eDuplicates.length || wDuplicates.length) { - console.log('onCallbacksChanged.requested', '[duplicates]', rDuplicates.length, pDuplicates.length, eDuplicates.length, wDuplicates.length); - } - - /* - 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: eAdded, removed: eRemoved } = pruneCallbacks(executing, paths); - const { added: wAdded, removed: wRemoved } = pruneCallbacks(watched, paths); - - if (rRemoved.length + pRemoved.length + eRemoved.length + wRemoved.length) { - console.log('onCallbacksChanged.requested', '[pruned]', rRemoved.length, pRemoved.length, eRemoved.length, wRemoved.length); - } - - /* - 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(requested, pendingCallbacks); - console.log('onCallbacksChanged.requested', '[readyCallbacks]', readyCallbacks); - - /* - If: - - there are `requested` callbacks - - no `requested` callback can be promoted to `prioritized` - - no callbacks are `prioritized`, `executing`, `watched` or `executed` - Then: - - the `requested` callbacks form a ciruclar dependency and can never be executed - - prune them out of `requested` - */ - const rCircular = ( - !readyCallbacks.length && - !prioritized.length && - !executing.length && - !watched.length && - !executed.length && - requested.length - ) ? requested : []; - - /* - 5. Prune callbacks that became irrelevant in their `executionGroup` - */ - const pendingGroups = groupBy( - cb => cb.executionGroup as any, - filter(cb => !isNil(cb.executionGroup), stored) - ); - console.log('onCallbacksChanged.requested', '[pendingGroups]', pendingGroups, map(pg => flatten(map( - gcb => gcb.executionMeta.updatedProps, - pg - )), values(pendingGroups))); - - const dropped: ICallback[] = filter(cb => { - if (!cb.executionGroup || !pendingGroups[cb.executionGroup] || !pendingGroups[cb.executionGroup].length) { - return false; - } - - const inputs = map(combineIdAndProp, flatten(cb.getInputs(paths))); - - const allProps = flatten(map( - gcb => gcb.executionMeta.allProps, - pendingGroups[cb.executionGroup] - )); - - const updated = flatten(map( - gcb => gcb.executionMeta.updatedProps, - pendingGroups[cb.executionGroup] - )); - - const res = - isEmpty(intersection( - inputs, - updated - )) && - isEmpty(difference( - inputs, - allProps - )) - && !all( - isMultiValued, - cb.callback.inputs - ); - - console.log('SPECIAL', cb, res, inputs, allProps, updated); - - return res; - }, - readyCallbacks - ); - - console.log('onCallbacksChanged.requested', '[dropped]', readyCallbacks, dropped, pendingGroups); - - /* - 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, - 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, - eRemoved.length ? removeExecutingCallbacks(eRemoved) : null, - eAdded.length ? addExecutingCallbacks(eAdded) : null, - wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, - wAdded.length ? addWatchedCallbacks(wAdded) : null, - // Prune circular callbacks - rCircular.length ? removeRequestedCallbacks(rCircular) : null, - // Drop non-triggered initial callbacks - dropped.length ? removeRequestedCallbacks(dropped) : null, - // Promote callbacks - readyCallbacks.length ? removeRequestedCallbacks(readyCallbacks) : null, - readyCallbacks.length ? addPrioritizedCallbacks(readyCallbacks) : null - ])); -}, ['callbacks.requested', 'callbacks.completed']); - -observe(async ({ - dispatch, - getState -}) => { - const { callbacks: { executing, watched }, config, hooks, layout, paths } = getState(); - let { callbacks: { prioritized } } = getState(); - - console.log('onCallbacksChanged.prioritized', prioritized); - - const available = Math.max( - 0, - 6 - executing.length - watched.length - ); - - prioritized = prioritized.slice(0, available); - if (!prioritized.length) { - return; - } - - const callbacks: [ICallback, any][] = prioritized.map(cb => { - 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 })); - }); - cb.requestedOutputs = reqOut; - - return [cb, { allOutputs, allPropIds }]; - }); - - const ids = reduce((res, [cb]) => ([ - ...res, - ...cb.getInputs(paths), - ...cb.getState(paths) - ]), [] as ICallbackProperty[], callbacks); - - /* - Make sure the app is ready to execute callbacks impacting `ids` - */ - await isAppReady(layout, paths, uniq(pluck('id', ids))); - - /* - Make sure to only execute callbacks that are still in the `prioritized` list (isAppReady is async - state could have changed) - */ - const { callbacks: { prioritized: updatedPrioritized } } = getState(); - const [remainingCallbacks] = partition( - ([cb]) => includes(cb, updatedPrioritized), - callbacks - ); - - const executingCallbacks: IExecutingCallback[] = remainingCallbacks.map(([cb, stash]) => { - return executeCallback(cb, config, hooks, paths, layout, stash); - }); - - dispatch(aggregateCallbacks([ - remainingCallbacks.length ? removePrioritizedCallbacks(map(([cb]) => cb, remainingCallbacks)) : null, - executingCallbacks.length ? addExecutingCallbacks(executingCallbacks) : null - ])); - -}, ['callbacks.prioritized', 'callbacks.completed']); - -observe(({ - dispatch, - getState -}) => { - const { - callbacks: { - executing - }, - } = getState(); - - console.log('onCallbacksChanged.executing', executing); - - 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 - ])); - - deferred.forEach(async function (cb: IExecutingCallback) { - const result = await cb.executionPromise; - - /* - Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled - */ - const { callbacks: { watched } } = getState(); - - /* - 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 - }]) - ])); - }); -}, ['callbacks.executing']); - -observe(({ - 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; - } - - console.log('onCallbacksChanged.executed', executed); - - let requestedCallbacks: ICallback[] = []; - let storedCallbacks: IStoredCallback[] = []; - - forEach(cb => { - const { - callback: { - clientside_function, - output - }, - executionResult - } = cb; - - if (isNil(executionResult)) { - return; - } - - const { data, error, payload } = executionResult; - console.log('onCallbacksChanged.executed', '[executionResult]', cb, data); - - 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); - - // Skip prop-triggered callbacks for callbacks with an execution group - these callbacks - // should already be present in `requested` - requestedCallbacks = concat( - requestedCallbacks, - flatten(map( - prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop), - keys(props) - )) - ); - - // 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)); - - requestedCallbacks = concat( - requestedCallbacks, - getLayoutCallbacks(graphs, paths, children, { - chunkPath: oldChildrenPath, - }) - ); - - // 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 - }) - ); - } - - // 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, paths } = getState(); - - requestedCallbacks = concat( - requestedCallbacks, - includeObservers(id, addedProps, graphs, paths) - ); - } - }, Object.entries(data)); - - - - 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); - - console.log('SPECIAL', '[requestedCallbacks]', requestedCallbacks); - dispatch(aggregateCallbacks([ - executed.length ? removeExecutedCallbacks(executed) : null, - executed.length ? addCompletedCallbacks(executed.length) : null, - storedCallbacks.length ? addStoredCallbacks(storedCallbacks) : null, - requestedCallbacks.length ? addRequestedCallbacks(requestedCallbacks) : null - ])); -}, ['callbacks.executed']); - -observe(({ - dispatch, - getState -}) => { - const { callbacks } = getState(); - const pendingCallbacks = getPendingCallbacks(callbacks); - - let { callbacks: { stored } } = getState(); - - console.log('onCallbacksChanged.stored', stored); - - 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, - callbacks - ]) => !pendingGroups[executionGroup] ? - concat(res, callbacks) : - res, - [] as IStoredCallback[], - toPairs(executionGroups) - ); - - console.log('onCallbacksChanged.stored', '[dropped]', nullGroupCallbacks, dropped); - - dispatch(aggregateCallbacks([ - nullGroupCallbacks.length ? removeStoredCallbacks(nullGroupCallbacks) : null, - dropped.length ? removeStoredCallbacks(dropped): null - ])); -}, ['callbacks.stored', 'callbacks.completed']) +observe(pendingCallbacks); +observe(requestedCallbacks); +observe(prioritizeCallbacks); +observe(executingCallbacks); +observe(executedCallbacks); +observe(storedCallbacks); const AppProvider = ({hooks}: any) => { return ( diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index b03cdb0264..6b15a58797 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -10,40 +10,53 @@ import { Store, Unsubscribe } from 'redux'; type Observer = (store: TStore) => void; type UnregisterObserver = () => void; -interface IStoreObserver { +interface IStoreObserverState { inputPaths: string[][]; lastState: any; observer: Observer; triggered: boolean; } -export default class StoreObserver { - private _store?: TStore; +export interface IStoreObserverDefinition { + observer: Observer>; + inputs: string[] +} + +export default class StoreObserver { + private _store?: Store; private _unsubscribe?: Unsubscribe; - private readonly _observers: IStoreObserver[] = []; + private readonly _observers: IStoreObserverState>[] = []; - constructor(store?: TStore) { + constructor(store?: Store) { this.__init__(store); } observe = ( - observer: Observer, - inputs: string[] + observer: IStoreObserverDefinition | Observer>, + inputs?: string[] ): UnregisterObserver => { - this.add(observer, inputs); - - return () => this.remove(observer); + 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: TStore) => { + setStore = (store: Store) => { this.__finalize__(); this.__init__(store); } private __finalize__ = () => this._unsubscribe?.() - private __init__ = (store?: TStore) => { + private __init__ = (store?: Store) => { this._store = store; if (store) { this._unsubscribe = store.subscribe(this.notify); @@ -53,7 +66,7 @@ export default class StoreObserver { } private add = ( - observer: Observer, + observer: Observer>, inputs: string[] ) => this._observers.push({ inputPaths: map(p => p.split('.'), inputs), @@ -67,7 +80,7 @@ export default class StoreObserver { this._observers ); - private notifyObserver = (o: IStoreObserver) => { + private notifyObserver = (o: IStoreObserverState>) => { const store = this._store; if (!store) { return; @@ -102,7 +115,7 @@ export default class StoreObserver { o.lastState = s; }; - private remove = (observer: Observer) => this._observers.splice( + private remove = (observer: Observer>) => this._observers.splice( this._observers.findIndex( o => observer === o.observer, this._observers diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts new file mode 100644 index 0000000000..9680754e0b --- /dev/null +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -0,0 +1,222 @@ +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, getCallbacksByInput } from '../actions/dependencies'; + +import { + combineIdAndProp, + 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; + } + + console.log('onCallbacksChanged.executed', executed); + + let requestedCallbacks: ICallback[] = []; + let storedCallbacks: IStoredCallback[] = []; + + forEach(cb => { + const { + callback: { + clientside_function, + output + }, + executionResult + } = cb; + + if (isNil(executionResult)) { + return; + } + + const { data, error, payload } = executionResult; + console.log('onCallbacksChanged.executed', '[executionResult]', cb, data); + + 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); + + // Skip prop-triggered callbacks for callbacks with an execution group - these callbacks + // should already be present in `requested` + requestedCallbacks = concat( + requestedCallbacks, + flatten(map( + prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop), + keys(props) + )) + ); + + // 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)); + + requestedCallbacks = concat( + requestedCallbacks, + getLayoutCallbacks(graphs, paths, children, { + chunkPath: oldChildrenPath, + }) + ); + + // 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 + }) + ); + } + + // 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, paths } = getState(); + + requestedCallbacks = concat( + requestedCallbacks, + includeObservers(id, addedProps, graphs, paths) + ); + } + }, Object.entries(data)); + + + + 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); + + console.log('SPECIAL', '[requestedCallbacks]', requestedCallbacks); + 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..5d23d59cc7 --- /dev/null +++ b/dash-renderer/src/observers/executingCallbacks.ts @@ -0,0 +1,73 @@ +import { + partition, + assoc, + find +} from 'ramda'; + +import { + aggregateCallbacks, + removeExecutingCallbacks, + removeWatchedCallbacks, + addWatchedCallbacks, + addExecutedCallbacks +} from '../actions/callbacks'; + +import { + IExecutingCallback, +} from '../types/callbacks'; +import { IStoreObserverDefinition } from '../StoreObserver'; +import { IStoreState } from '../store'; + +const observer: IStoreObserverDefinition = { + observer: ({ + dispatch, + getState + }) => { + const { + callbacks: { + executing + }, + } = getState(); + + console.log('onCallbacksChanged.executing', executing); + + 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 + ])); + + deferred.forEach(async function (cb: IExecutingCallback) { + const result = await cb.executionPromise; + + /* + Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled + */ + const { callbacks: { watched } } = getState(); + + /* + 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 + }]) + ])); + }); + }, + inputs: ['callbacks.executing'] +}; + +export default observer; diff --git a/dash-renderer/src/observers/pendingCallbacks.ts b/dash-renderer/src/observers/pendingCallbacks.ts new file mode 100644 index 0000000000..df0cab438d --- /dev/null +++ b/dash-renderer/src/observers/pendingCallbacks.ts @@ -0,0 +1,36 @@ +import { setPendingCallbacks } from '../actions/callbacks'; +import { getPendingCallbacks } from '../utils/callbacks'; +import { IStoreObserverDefinition } from '../StoreObserver'; +import { IStoreState } from '../store'; + +const observer: IStoreObserverDefinition = { + observer: ({ + dispatch, + getState + }) => { + const { + callbacks, + pendingCallbacks + } = getState(); + + const next = getPendingCallbacks(callbacks); + + /** + * If the calculated list of pending callbacks is equivalent + * to the previous one, do not update it. + */ + if ( + pendingCallbacks && + pendingCallbacks.length === next.length && + next.every((v, i) => + v === pendingCallbacks[i] || + v.callback === pendingCallbacks[i].callback) + ) { + return; + } + + dispatch(setPendingCallbacks(next)); + }, inputs: ['callbacks'] +}; + +export default observer; \ No newline at end of file diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts new file mode 100644 index 0000000000..a1d747388b --- /dev/null +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -0,0 +1,106 @@ +import { + flatten, + map, + reduce, + uniq, + pluck, + partition, + includes +} from 'ramda'; + +import { IStoreState } from "../store"; + +import { + aggregateCallbacks, + removePrioritizedCallbacks, + addExecutingCallbacks, + executeCallback +} from '../actions/callbacks'; + +import { stringifyId } from '../actions/dependencies'; + +import { + combineIdAndProp +} from '../actions/dependencies_ts'; + +import isAppReady from '../actions/isAppReady'; + +import { + ICallback, + IExecutingCallback, + ICallbackProperty +} from '../types/callbacks'; +import { IStoreObserverDefinition } from '../StoreObserver'; + +const observer: IStoreObserverDefinition = { + observer: async ({ + dispatch, + getState + }) => { + const { callbacks: { executing, watched }, config, hooks, layout, paths } = getState(); + let { callbacks: { prioritized } } = getState(); + + console.log('onCallbacksChanged.prioritized', prioritized); + + const available = Math.max( + 0, + 6 - executing.length - watched.length + ); + + prioritized = prioritized.slice(0, available); + if (!prioritized.length) { + return; + } + + const callbacks: [ICallback, any][] = prioritized.map(cb => { + 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 })); + }); + cb.requestedOutputs = reqOut; + + return [cb, { allOutputs, allPropIds }]; + }); + + const ids = reduce((res, [cb]) => ([ + ...res, + ...cb.getInputs(paths), + ...cb.getState(paths) + ]), [] as ICallbackProperty[], callbacks); + + /* + Make sure the app is ready to execute callbacks impacting `ids` + */ + await isAppReady(layout, paths, uniq(pluck('id', ids))); + + /* + Make sure to only execute callbacks that are still in the `prioritized` list (isAppReady is async - state could have changed) + */ + const { callbacks: { prioritized: updatedPrioritized } } = getState(); + const [remainingCallbacks] = partition( + ([cb]) => includes(cb, updatedPrioritized), + callbacks + ); + + const executingCallbacks: IExecutingCallback[] = remainingCallbacks.map(([cb, stash]) => { + return executeCallback(cb, config, hooks, paths, layout, stash); + }); + + dispatch(aggregateCallbacks([ + remainingCallbacks.length ? removePrioritizedCallbacks(map(([cb]) => cb, remainingCallbacks)) : null, + executingCallbacks.length ? addExecutingCallbacks(executingCallbacks) : null + ])); + + }, + inputs: ['callbacks.prioritized', 'callbacks.completed'] +}; + +export default observer; \ No newline at end of file diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts new file mode 100644 index 0000000000..533c4b86ad --- /dev/null +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -0,0 +1,274 @@ +import { + all, + concat, + difference, + filter, + flatten, + groupBy, + intersection, + isEmpty, + isNil, + map, + values +} from 'ramda'; + +import { IStoreState } from "../store"; + +import { + aggregateCallbacks, + removeRequestedCallbacks, + removePrioritizedCallbacks, + removeExecutingCallbacks, + removeWatchedCallbacks, + addRequestedCallbacks, + addPrioritizedCallbacks, + addExecutingCallbacks, + addWatchedCallbacks +} from '../actions/callbacks'; + +import { isMultiValued } from '../actions/dependencies'; + +import { + combineIdAndProp, + getReadyCallbacks, + getUniqueIdentifier, + pruneCallbacks +} from '../actions/dependencies_ts'; + +import { + ICallback, + IExecutingCallback, + IStoredCallback +} from '../types/callbacks'; + +import { getPendingCallbacks } from "../utils/callbacks"; +import { IStoreObserverDefinition } from '../StoreObserver'; + +const observer: IStoreObserverDefinition = { + observer: ({ + dispatch, + getState + }) => { + const { callbacks, callbacks: { prioritized, executing, watched, executed, completed, stored }, paths } = getState(); + let { callbacks: { requested } } = getState(); + + const pendingCallbacks = getPendingCallbacks(callbacks); + + console.log('onCallbacksChanged.requested', requested, completed, callbacks); + + /* + 1. Remove duplicated `requested` callbacks + */ + + /* + Extract all but the first callback from each IOS-key group + these callbacks are duplicates. + */ + const rDuplicates = flatten(map( + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(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), + // group => filter(cb => !cb.executionGroup, group).slice(1), + values( + groupBy( + getUniqueIdentifier, + concat(prioritized, requested) + ) + ) + )); + + const eDuplicates = flatten(map( + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), + values( + groupBy( + getUniqueIdentifier, + concat(executing, requested) + ) + ) + )) as IExecutingCallback[]; + + const wDuplicates = flatten(map( + group => group.slice(0, -1), + // group => filter(cb => !cb.executionGroup, group).slice(1), + values( + groupBy( + getUniqueIdentifier, + concat(watched, requested) + ) + ) + )) as IExecutingCallback[]; + + if (rDuplicates.length || pDuplicates.length || eDuplicates.length || wDuplicates.length) { + console.log('onCallbacksChanged.requested', '[duplicates]', rDuplicates.length, pDuplicates.length, eDuplicates.length, wDuplicates.length); + } + + /* + 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: eAdded, removed: eRemoved } = pruneCallbacks(executing, paths); + const { added: wAdded, removed: wRemoved } = pruneCallbacks(watched, paths); + + if (rRemoved.length + pRemoved.length + eRemoved.length + wRemoved.length) { + console.log('onCallbacksChanged.requested', '[pruned]', rRemoved.length, pRemoved.length, eRemoved.length, wRemoved.length); + } + + /* + 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(requested, pendingCallbacks); + console.log('onCallbacksChanged.requested', '[readyCallbacks]', readyCallbacks); + + /* + If: + - there are `requested` callbacks + - no `requested` callback can be promoted to `prioritized` + - no callbacks are `prioritized`, `executing`, `watched` or `executed` + Then: + - the `requested` callbacks form a ciruclar dependency and can never be executed + - prune them out of `requested` + */ + const rCircular = ( + !readyCallbacks.length && + !prioritized.length && + !executing.length && + !watched.length && + !executed.length && + requested.length + ) ? requested : []; + + /* + 5. Prune callbacks that became irrelevant in their `executionGroup` + */ + const pendingGroups = groupBy( + cb => cb.executionGroup as any, + filter(cb => !isNil(cb.executionGroup), stored) + ); + console.log('onCallbacksChanged.requested', '[pendingGroups]', pendingGroups, map(pg => flatten(map( + gcb => gcb.executionMeta.updatedProps, + pg + )), values(pendingGroups))); + + const dropped: ICallback[] = filter(cb => { + if (!cb.executionGroup || !pendingGroups[cb.executionGroup] || !pendingGroups[cb.executionGroup].length) { + return false; + } + + const inputs = map(combineIdAndProp, flatten(cb.getInputs(paths))); + + const allProps = flatten(map( + gcb => gcb.executionMeta.allProps, + pendingGroups[cb.executionGroup] + )); + + const updated = flatten(map( + gcb => gcb.executionMeta.updatedProps, + pendingGroups[cb.executionGroup] + )); + + const res = + isEmpty(intersection( + inputs, + updated + )) && + isEmpty(difference( + inputs, + allProps + )) + && !all( + isMultiValued, + cb.callback.inputs + ); + + console.log('SPECIAL', cb, res, inputs, allProps, updated); + + return res; + }, + readyCallbacks + ); + + console.log('onCallbacksChanged.requested', '[dropped]', readyCallbacks, dropped, pendingGroups); + + /* + 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, + 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, + eRemoved.length ? removeExecutingCallbacks(eRemoved) : null, + eAdded.length ? addExecutingCallbacks(eAdded) : null, + wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, + wAdded.length ? addWatchedCallbacks(wAdded) : null, + // Prune circular callbacks + rCircular.length ? removeRequestedCallbacks(rCircular) : 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; \ No newline at end of file diff --git a/dash-renderer/src/observers/storedCallbacks.ts b/dash-renderer/src/observers/storedCallbacks.ts new file mode 100644 index 0000000000..25d5589190 --- /dev/null +++ b/dash-renderer/src/observers/storedCallbacks.ts @@ -0,0 +1,73 @@ +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(); + + console.log('onCallbacksChanged.stored', stored); + + 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, + callbacks + ]) => !pendingGroups[executionGroup] ? + concat(res, callbacks) : + res, + [] as IStoredCallback[], + toPairs(executionGroups) + ); + + console.log('onCallbacksChanged.stored', '[dropped]', nullGroupCallbacks, dropped); + + 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/store.ts b/dash-renderer/src/store.ts index 9d1c347ac8..b008129e44 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -1,23 +1,26 @@ -import { createStore, applyMiddleware, Store } from 'redux'; +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 { ICallback } from './types/callbacks'; -interface IStoreState { +export interface IStoreState { callbacks: ICallbacksState; pendingCallbacks: ICallback[]; [key: string]: any; } -type DashStore = Store; - -let store: DashStore; -const storeObserver = new StoreObserver(); +let store: Store; +const storeObserver = new StoreObserver(); export const observe = storeObserver.observe; +export interface IStoreObserver { + observer: Observer>; + inputs: string[]; +} + function createAppStore(reducer: any, middleware: any) { store = createStore(reducer, middleware); storeObserver.setStore(store); @@ -31,7 +34,7 @@ function createAppStore(reducer: any, middleware: any) { * @returns {Store} * An initialized redux store with middleware and possible hot reloading of reducers */ -const initializeStore = (reset?: boolean): DashStore => { +const initializeStore = (reset?: boolean): Store => { if (store && !reset) { return store; } diff --git a/dash-renderer/src/utils/callbacks.ts b/dash-renderer/src/utils/callbacks.ts new file mode 100644 index 0000000000..4d8ffbfe0a --- /dev/null +++ b/dash-renderer/src/utils/callbacks.ts @@ -0,0 +1,15 @@ +import { ICallbacksState } from '../reducers/callbacks'; + +export const getPendingCallbacks = ({ + executed, + executing, + prioritized, + requested, + watched +}: ICallbacksState) => [ + ...requested, + ...prioritized, + ...executing, + ...watched, + ...executed +]; \ No newline at end of file From 297d9cd9b408b271499bd8a3b66500e2e703cb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 17:25:22 -0400 Subject: [PATCH 38/90] - add `loadingMap` to store - use `loadingMap` instead of `pendingCallbacks` for loading_state resolution --- dash-renderer/src/StoreObserver.ts | 55 +++++------ dash-renderer/src/TreeContainer.js | 97 +++++++------------ dash-renderer/src/actions/loadingMap.ts | 5 + .../src/observers/pendingCallbacks.ts | 86 +++++++++++++--- dash-renderer/src/reducers/loadingMap.ts | 22 +++++ dash-renderer/src/reducers/reducer.js | 2 + dash-renderer/src/store.ts | 2 + dash-renderer/src/types/callbacks.ts | 10 +- 8 files changed, 169 insertions(+), 110 deletions(-) create mode 100644 dash-renderer/src/actions/loadingMap.ts create mode 100644 dash-renderer/src/reducers/loadingMap.ts diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index 6b15a58797..4c49dced11 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -1,5 +1,6 @@ import { any, + filter, forEach, map, path @@ -75,45 +76,33 @@ export default class StoreObserver { triggered: false }); - private notify = () => forEach( - this.notifyObserver, - this._observers - ); - - private notifyObserver = (o: IStoreObserverState>) => { + private notify = () => { const store = this._store; if (!store) { return; } - const state: any = store.getState(); - - /** Don't trigger if nested */ - if (o.triggered) { - return; - } - - const { inputPaths, lastState, observer } = o; + const state = store.getState(); - /** Don't notify observer if there's no change */ - if (!any( - i => path(i, state) !== path(i, lastState), - inputPaths - )) { - return; - } - - o.triggered = true; - /** - * Due to nested store updates, the state could change between the - * observer call and setting `lastState`, leading to untriggered changes - */ - const s = store.getState(); - observer(store); - - o.triggered = false; - o.lastState = s; - }; + 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( diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index a2437829ab..795a999192 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -8,13 +8,14 @@ import { concat, dissoc, equals, - filter, + find, has, isEmpty, isNil, keys, map, mergeRight, + path, pick, pickBy, propOr, @@ -265,81 +266,50 @@ function isLoadingComponent(layout) { return Registry.resolve(layout)._dashprivate_isLoadingComponent; } -function getNestedIds(layout) { - const ids = []; - const queue = [layout]; - - while (queue.length) { - const elementLayout = queue.shift(); - - const props = elementLayout && elementLayout.props; - - if (!props) { - continue; - } - - const {children, id} = props; - - if (id) { - ids.push(id); - } - - if (children) { - const filteredChildren = filter( - child => - !isSimpleComponent(child) && !isLoadingComponent(child), - Array.isArray(children) ? children : [children] - ); - - queue.push(...filteredChildren); - } +function getLoadingState(componentLayout, componentPath, loadingMap) { + if (isNil(loadingMap)) { + return { + is_loading: false, + }; } - 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; + const loadingFragment = path(componentPath, loadingMap); + // Component and children are not loading if there's no loading fragment + // for the component's path in the layout. + if (isNil(loadingFragment)) { + return { + is_loading: false, + }; + } - if (pendingCallbacks && pendingCallbacks.length && ids && ids.length) { - const idStrs = ids.map(stringifyId); + const {__dashprivate__idprop__: ids} = loadingFragment; - pendingCallbacks.forEach(cb => { - const {executionPromise, requestedOutputs} = cb; - if (executionPromise === undefined) { - return; - } + if (isLoadingComponent(componentLayout)) { + return { + is_loading: true, + prop_name: ids[0].property, + component_name: ids[0].id, + }; + } - idStrs.forEach(idStr => { - const props = requestedOutputs[idStr]; - if (props) { - isLoading = true; - // TODO: what about multiple loading components / props? - loadingComponent = idStr; - loadingProp = props[0]; - } - }); - }); + const entry = find(id => id.id === componentLayout.props.id, ids); + if (entry) { + return { + is_loading: true, + prop_name: entry.property, + component_name: entry.id, + }; } - // Set loading state return { - is_loading: isLoading, - prop_name: loadingProp, - component_name: loadingComponent, + is_loading: false, }; } export const AugmentedTreeContainer = connect( state => ({ graphs: state.graphs, - pendingCallbacks: state.pendingCallbacks, + loadingMap: state.loadingMap, config: state.config, }), dispatch => ({dispatch}), @@ -350,7 +320,8 @@ export const AugmentedTreeContainer = connect( _dashprivate_path: ownProps._dashprivate_path, _dashprivate_loadingState: getLoadingState( ownProps._dashprivate_layout, - stateProps.pendingCallbacks + ownProps._dashprivate_path, + stateProps.loadingMap ), _dashprivate_config: stateProps.config, }) diff --git a/dash-renderer/src/actions/loadingMap.ts b/dash-renderer/src/actions/loadingMap.ts new file mode 100644 index 0000000000..1d76ad8fa7 --- /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); \ No newline at end of file diff --git a/dash-renderer/src/observers/pendingCallbacks.ts b/dash-renderer/src/observers/pendingCallbacks.ts index df0cab438d..32f920edec 100644 --- a/dash-renderer/src/observers/pendingCallbacks.ts +++ b/dash-renderer/src/observers/pendingCallbacks.ts @@ -1,4 +1,14 @@ +import { + equals, + flatten, + forEach, + isEmpty, + map, + reduce +} from 'ramda'; + import { setPendingCallbacks } from '../actions/callbacks'; +import { setLoadingMap } from '../actions/loadingMap'; import { getPendingCallbacks } from '../utils/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; @@ -10,27 +20,81 @@ const observer: IStoreObserverDefinition = { }) => { const { callbacks, + callbacks: { + executing, + watched, + executed + }, + loadingMap, + paths, pendingCallbacks } = getState(); - const next = getPendingCallbacks(callbacks); + console.log('onCallbacksChanged.pendingCallbacks', callbacks); + + /* + Get the path of all components impacted by callbacks + with states: executing, watched, executed + */ + + const loadingPaths = flatten(map( + cb => cb.getOutputs(paths), + [...executing, ...watched, ...executed] + )); + + const nextMap: any = isEmpty(loadingPaths) ? + null : + reduce( + (res, path) => { + let target = res; + target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; + target.__dashprivate__idprop__.push({ + id: path.id, + property: path.property + }); + + forEach(p => { + target = (target[p] = + target[p] ?? + p === 'children' ? [] : {} + ) + + target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; + target.__dashprivate__idprop__.push({ + id: path.id, + property: path.property + }); - /** - * If the calculated list of pending callbacks is equivalent - * to the previous one, do not update it. + }, path.path); + + return res; + }, + {} as any, + loadingPaths + ); + + if (!equals(nextMap, loadingMap)) { + console.log('SPECIAL', '[setLoadingMap]', nextMap); + dispatch(setLoadingMap(nextMap)); + } + + /* + * If the calculated list of pending callbacks is not + * equivalent to the current one, update it. */ + const next = getPendingCallbacks(callbacks); if ( - pendingCallbacks && - pendingCallbacks.length === next.length && - next.every((v, i) => + !pendingCallbacks || + pendingCallbacks.length !== next.length || + !next.every((v, i) => v === pendingCallbacks[i] || v.callback === pendingCallbacks[i].callback) ) { - return; + console.log('SPECIAL', '[setPendingCallbacks]', next); + dispatch(setPendingCallbacks(next)); } - - dispatch(setPendingCallbacks(next)); - }, inputs: ['callbacks'] + }, + inputs: ['callbacks'] }; export default observer; \ No newline at end of file diff --git a/dash-renderer/src/reducers/loadingMap.ts b/dash-renderer/src/reducers/loadingMap.ts new file mode 100644 index 0000000000..7c22d490bd --- /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; \ No newline at end of file diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 58760cc879..9600aff972 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -12,6 +12,7 @@ import error from './error'; import history from './history'; import hooks from './hooks'; import layout from './layout'; +import loadingMap from './loadingMap'; import paths from './paths'; import pendingCallbacks from './pendingCallbacks'; @@ -32,6 +33,7 @@ function mainReducer() { history, hooks, layout, + loadingMap, paths, pendingCallbacks, }; diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index b008129e44..5b9d3934df 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -3,10 +3,12 @@ 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 { ICallback } from './types/callbacks'; export interface IStoreState { callbacks: ICallbacksState; + loadingMap: LoadingMapState; pendingCallbacks: ICallback[]; [key: string]: any; } diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 407c8c4078..eb36c8e14e 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -5,6 +5,10 @@ export interface ICallbackProperty { property: string; } +export interface ILayoutCallbackProperty extends ICallbackProperty { + path: (string | number)[]; +} + export interface ICallback { anyVals: any[] | string; callback: { @@ -19,9 +23,9 @@ export interface ICallback { state: ICallbackProperty[]; }; executionGroup?: string; - getInputs: (paths: any) => ICallbackProperty[]; - getOutputs: (paths: any) => ICallbackProperty[]; - getState: (paths: any) => ICallbackProperty[]; + getInputs: (paths: any) => ILayoutCallbackProperty[]; + getOutputs: (paths: any) => ILayoutCallbackProperty[]; + getState: (paths: any) => ILayoutCallbackProperty[]; prevent_initial_call: boolean; [key: string]: any; From 0621dfdec1511cababcde472f6859c7d965f17fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 17:46:19 -0400 Subject: [PATCH 39/90] remove `pendingCallbacks` store prop --- dash-renderer/src/AppProvider.react.tsx | 4 +-- dash-renderer/src/TreeContainer.js | 2 +- dash-renderer/src/actions/callbacks.ts | 2 -- .../components/core/DocumentTitle.react.js | 7 +++-- .../src/components/core/Loading.react.js | 7 +++-- .../{pendingCallbacks.ts => loadingMap.ts} | 28 +++---------------- .../src/reducers/pendingCallbacks.js | 11 -------- dash-renderer/src/reducers/reducer.js | 2 -- dash-renderer/src/store.ts | 2 -- 9 files changed, 15 insertions(+), 50 deletions(-) rename dash-renderer/src/observers/{pendingCallbacks.ts => loadingMap.ts} (69%) delete mode 100644 dash-renderer/src/reducers/pendingCallbacks.js diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 897bfa6493..b087585609 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -5,7 +5,7 @@ import {Provider} from 'react-redux'; import initializeStore, { observe } from './store'; import AppContainer from './AppContainer.react'; -import pendingCallbacks from './observers/pendingCallbacks'; +import loadingMap from './observers/loadingMap'; import requestedCallbacks from './observers/requestedCallbacks'; import prioritizeCallbacks from './observers/prioritizedCallbacks'; import executingCallbacks from './observers/executingCallbacks'; @@ -13,7 +13,7 @@ import executedCallbacks from './observers/executedCallbacks'; import storedCallbacks from './observers/storedCallbacks'; const store = initializeStore(); -observe(pendingCallbacks); +observe(loadingMap); observe(requestedCallbacks); observe(prioritizeCallbacks); observe(executingCallbacks); diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 795a999192..0a5322e20a 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -292,7 +292,7 @@ function getLoadingState(componentLayout, componentPath, loadingMap) { }; } - const entry = find(id => id.id === componentLayout.props.id, ids); + const entry = find(id => id.id === componentLayout.props.id, ids ?? []); if (entry) { return { is_loading: true, diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index a9aa668ad2..5209aff069 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -18,8 +18,6 @@ import { urlBase } from './utils'; import { getCSRFHeader } from '.'; import { createAction, Action } from 'redux-actions'; -export const setPendingCallbacks = createAction('SET_PENDING_CALLBACKS'); - export const addCompletedCallbacks = createAction( CallbackAggregateActionType.AddCompleted ); diff --git a/dash-renderer/src/components/core/DocumentTitle.react.js b/dash-renderer/src/components/core/DocumentTitle.react.js index 46eba06bfc..140a427263 100644 --- a/dash-renderer/src/components/core/DocumentTitle.react.js +++ b/dash-renderer/src/components/core/DocumentTitle.react.js @@ -1,6 +1,7 @@ import {connect} from 'react-redux'; import {Component} from 'react'; import PropTypes from 'prop-types'; +import {isEmpty} from 'ramda'; class DocumentTitle extends Component { constructor(props) { @@ -11,7 +12,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 +29,9 @@ class DocumentTitle extends Component { } DocumentTitle.propTypes = { - pendingCallbacks: PropTypes.array.isRequired, + isLoading: PropTypes.any.isRequired, }; export default connect(state => ({ - pendingCallbacks: state.pendingCallbacks, + isLoading: !isEmpty(state.loadingMap?.__dashprivate__idprops), }))(DocumentTitle); diff --git a/dash-renderer/src/components/core/Loading.react.js b/dash-renderer/src/components/core/Loading.react.js index 999684a8dc..8ec9ef3ff7 100644 --- a/dash-renderer/src/components/core/Loading.react.js +++ b/dash-renderer/src/components/core/Loading.react.js @@ -1,18 +1,19 @@ import {connect} from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; +import {isEmpty} from 'ramda'; function Loading(props) { - if (props.pendingCallbacks.length) { + if (props.isLoading) { return
; } return null; } Loading.propTypes = { - pendingCallbacks: PropTypes.array.isRequired, + isLoading: PropTypes.any.isRequired, }; export default connect(state => ({ - pendingCallbacks: state.pendingCallbacks, + isLoading: !isEmpty(state.loadingMap?.__dashprivate__idprops), }))(Loading); diff --git a/dash-renderer/src/observers/pendingCallbacks.ts b/dash-renderer/src/observers/loadingMap.ts similarity index 69% rename from dash-renderer/src/observers/pendingCallbacks.ts rename to dash-renderer/src/observers/loadingMap.ts index 32f920edec..78d304094e 100644 --- a/dash-renderer/src/observers/pendingCallbacks.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -7,9 +7,7 @@ import { reduce } from 'ramda'; -import { setPendingCallbacks } from '../actions/callbacks'; import { setLoadingMap } from '../actions/loadingMap'; -import { getPendingCallbacks } from '../utils/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; @@ -19,18 +17,17 @@ const observer: IStoreObserverDefinition = { getState }) => { const { - callbacks, callbacks: { executing, watched, executed }, loadingMap, - paths, - pendingCallbacks + paths } = getState(); - console.log('onCallbacksChanged.pendingCallbacks', callbacks); + const callbacks = [...executing, ...watched, ...executed]; + console.log('onCallbacksChanged.loadingMap', callbacks); /* Get the path of all components impacted by callbacks @@ -74,27 +71,10 @@ const observer: IStoreObserverDefinition = { ); if (!equals(nextMap, loadingMap)) { - console.log('SPECIAL', '[setLoadingMap]', nextMap); dispatch(setLoadingMap(nextMap)); } - - /* - * If the calculated list of pending callbacks is not - * equivalent to the current one, update it. - */ - const next = getPendingCallbacks(callbacks); - if ( - !pendingCallbacks || - pendingCallbacks.length !== next.length || - !next.every((v, i) => - v === pendingCallbacks[i] || - v.callback === pendingCallbacks[i].callback) - ) { - console.log('SPECIAL', '[setPendingCallbacks]', next); - dispatch(setPendingCallbacks(next)); - } }, - inputs: ['callbacks'] + inputs: ['callbacks.executing', 'callbacks.watched', 'callbacks.executed'] }; export default observer; \ No newline at end of file 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 9600aff972..87b40344f8 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -14,7 +14,6 @@ import hooks from './hooks'; import layout from './layout'; import loadingMap from './loadingMap'; import paths from './paths'; -import pendingCallbacks from './pendingCallbacks'; export const apiRequests = [ 'dependenciesRequest', @@ -35,7 +34,6 @@ function mainReducer() { layout, loadingMap, paths, - pendingCallbacks, }; forEach(r => { parts[r] = createApiReducer(r); diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index 5b9d3934df..f6316b39a1 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -4,12 +4,10 @@ import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; import { ICallbacksState } from './reducers/callbacks'; import { LoadingMapState } from './reducers/loadingMap'; -import { ICallback } from './types/callbacks'; export interface IStoreState { callbacks: ICallbacksState; loadingMap: LoadingMapState; - pendingCallbacks: ICallback[]; [key: string]: any; } From caf5ebe0f4a75c5f01f7c16148724ebf76de609c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 18:00:27 -0400 Subject: [PATCH 40/90] remove `pendingCallbacks` in tests --- dash/testing/dash_page.py | 15 +++++++-------- .../integration/callbacks/test_basic_callback.py | 4 ++-- .../callbacks/test_layout_paths_with_callbacks.py | 2 +- .../integration/callbacks/test_missing_inputs.py | 2 +- .../callbacks/test_multiple_callbacks.py | 2 +- tests/integration/renderer/test_dependencies.py | 2 +- tests/integration/renderer/test_due_diligence.py | 2 +- tests/integration/test_render.py | 12 ++++++++++-- 8 files changed, 24 insertions(+), 17 deletions(-) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 63b30d407a..da37d0efb2 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -36,13 +36,12 @@ 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; - }) + var loadingMap = window.store.getState().loadingMap; + if (!loadingMap || !loadingMap.__dashprivate__idprop__) { + return 0; + } + + return loadingMap.__dashprivate__idprop__.length; """ ) @@ -51,7 +50,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 == 0 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..9776d8e048 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 dash_duo.redux_state_rqs == 0 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 dash_duo.redux_state_rqs == 0, "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_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 80656d5b5f..362bc40729 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 dash_duo.redux_state_rqs == 0, "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..3117d146f3 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: dash_duo.redux_state_rqs == 0, 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..71e3bd6689 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 dash_duo.redux_state_rqs == 0 dash_duo.percy_snapshot( name="test_callbacks_called_multiple_times_and_out_of_order" diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index 6213d71f7b..52f0e7e223 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 dash_duo.redux_state_rqs == 0 assert dash_duo.get_logs() == [] diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py index cb44d39fb0..4b330f515f 100644 --- a/tests/integration/renderer/test_due_diligence.py +++ b/tests/integration/renderer/test_due_diligence.py @@ -94,7 +94,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 dash_duo.redux_state_rqs == 0, "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..cf2749aa5e 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -491,9 +491,17 @@ def update_output(n_clicks): 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" + """ + var loadingMap = window.store.getState().loadingMap; + if (!loadingMap || !loadingMap.__dashprivate__idprop__) { + return 0; + } + + return loadingMap.__dashprivate__idprop__.length; + """ ) - self.assertEqual(pending_count, 0) + + assert pending_count == 0 def test_callbacks_with_shared_grandparent(self): app = Dash() From 1a5e3fba966d814fe941cd72486c61f4b5432c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 19:27:36 -0400 Subject: [PATCH 41/90] add `isLoading` state to store (derived from callbacks) + use `isLoading` instead of `loadingMap` when appropriate --- dash-renderer/src/AppProvider.react.tsx | 8 ++++-- dash-renderer/src/actions/isLoading.ts | 5 ++++ .../components/core/DocumentTitle.react.js | 5 ++-- .../src/components/core/Loading.react.js | 5 ++-- dash-renderer/src/observers/isLoading.ts | 28 +++++++++++++++++++ dash-renderer/src/reducers/isLoading.ts | 22 +++++++++++++++ dash-renderer/src/reducers/reducer.js | 2 ++ dash-renderer/src/store.ts | 2 ++ 8 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 dash-renderer/src/actions/isLoading.ts create mode 100644 dash-renderer/src/observers/isLoading.ts create mode 100644 dash-renderer/src/reducers/isLoading.ts diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index b087585609..a26c32dbc7 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -5,14 +5,16 @@ import {Provider} from 'react-redux'; import initializeStore, { observe } from './store'; import AppContainer from './AppContainer.react'; +import executedCallbacks from './observers/executedCallbacks'; +import executingCallbacks from './observers/executingCallbacks'; +import isLoading from './observers/isLoading' import loadingMap from './observers/loadingMap'; -import requestedCallbacks from './observers/requestedCallbacks'; import prioritizeCallbacks from './observers/prioritizedCallbacks'; -import executingCallbacks from './observers/executingCallbacks'; -import executedCallbacks from './observers/executedCallbacks'; +import requestedCallbacks from './observers/requestedCallbacks'; import storedCallbacks from './observers/storedCallbacks'; const store = initializeStore(); +observe(isLoading); observe(loadingMap); observe(requestedCallbacks); observe(prioritizeCallbacks); diff --git a/dash-renderer/src/actions/isLoading.ts b/dash-renderer/src/actions/isLoading.ts new file mode 100644 index 0000000000..3842a2cfb8 --- /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); \ No newline at end of file diff --git a/dash-renderer/src/components/core/DocumentTitle.react.js b/dash-renderer/src/components/core/DocumentTitle.react.js index 140a427263..e192f32ce1 100644 --- a/dash-renderer/src/components/core/DocumentTitle.react.js +++ b/dash-renderer/src/components/core/DocumentTitle.react.js @@ -1,7 +1,6 @@ import {connect} from 'react-redux'; import {Component} from 'react'; import PropTypes from 'prop-types'; -import {isEmpty} from 'ramda'; class DocumentTitle extends Component { constructor(props) { @@ -29,9 +28,9 @@ class DocumentTitle extends Component { } DocumentTitle.propTypes = { - isLoading: PropTypes.any.isRequired, + isLoading: PropTypes.bool.isRequired, }; export default connect(state => ({ - isLoading: !isEmpty(state.loadingMap?.__dashprivate__idprops), + 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 8ec9ef3ff7..b4eb2793f5 100644 --- a/dash-renderer/src/components/core/Loading.react.js +++ b/dash-renderer/src/components/core/Loading.react.js @@ -1,7 +1,6 @@ import {connect} from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; -import {isEmpty} from 'ramda'; function Loading(props) { if (props.isLoading) { @@ -11,9 +10,9 @@ function Loading(props) { } Loading.propTypes = { - isLoading: PropTypes.any.isRequired, + isLoading: PropTypes.bool.isRequired, }; export default connect(state => ({ - isLoading: !isEmpty(state.loadingMap?.__dashprivate__idprops), + isLoading: state.isLoading, }))(Loading); diff --git a/dash-renderer/src/observers/isLoading.ts b/dash-renderer/src/observers/isLoading.ts new file mode 100644 index 0000000000..36f25e6221 --- /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; \ No newline at end of file diff --git a/dash-renderer/src/reducers/isLoading.ts b/dash-renderer/src/reducers/isLoading.ts new file mode 100644 index 0000000000..2ada3915e5 --- /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; \ No newline at end of file diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 87b40344f8..3fd04113df 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -11,6 +11,7 @@ import graphs from './dependencyGraph'; import error from './error'; import history from './history'; import hooks from './hooks'; +import isLoading from './isLoading'; import layout from './layout'; import loadingMap from './loadingMap'; import paths from './paths'; @@ -31,6 +32,7 @@ function mainReducer() { graphs, history, hooks, + isLoading, layout, loadingMap, paths, diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index f6316b39a1..bb87a5fdb2 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -4,9 +4,11 @@ import {createReducer} from './reducers/reducer'; import StoreObserver from './StoreObserver'; import { ICallbacksState } from './reducers/callbacks'; import { LoadingMapState } from './reducers/loadingMap'; +import { IsLoadingState } from './reducers/isLoading'; export interface IStoreState { callbacks: ICallbacksState; + isLoading: IsLoadingState; loadingMap: LoadingMapState; [key: string]: any; } From 91b0e898935073c6a1d77418238cbcbd7f5db66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 19:36:29 -0400 Subject: [PATCH 42/90] update `redux_state_rqs` implementation and usage --- dash/testing/dash_page.py | 9 ++------- tests/integration/callbacks/test_basic_callback.py | 4 ++-- .../callbacks/test_layout_paths_with_callbacks.py | 2 +- tests/integration/callbacks/test_missing_inputs.py | 2 +- .../integration/callbacks/test_multiple_callbacks.py | 2 +- tests/integration/devtools/test_hot_reload.py | 8 +++----- tests/integration/renderer/test_dependencies.py | 2 +- tests/integration/renderer/test_due_diligence.py | 2 +- tests/integration/test_render.py | 11 +++-------- 9 files changed, 15 insertions(+), 27 deletions(-) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index da37d0efb2..43523dcad5 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -36,12 +36,7 @@ def redux_state_paths(self): def redux_state_rqs(self): return self.driver.execute_script( """ - var loadingMap = window.store.getState().loadingMap; - if (!loadingMap || !loadingMap.__dashprivate__idprop__) { - return 0; - } - - return loadingMap.__dashprivate__idprop__.length; + return !window.store.getState().isLoading; """ ) @@ -50,7 +45,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 == 0 + 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 9776d8e048..799bed9d95 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 == 0 + assert dash_duo.redux_state_rqs 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 == 0, "loadingMap is empty" + assert dash_duo.redux_state_rqs, "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_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 362bc40729..1c3c4e5d30 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 == 0, "loadingMap is empty" + assert dash_duo.redux_state_rqs, "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 3117d146f3..1810d83421 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 == 0, 3) + wait.until(lambda: dash_duo.redux_state_rqs, 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 71e3bd6689..3c21c2a716 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 == 0 + assert dash_duo.redux_state_rqs dash_duo.percy_snapshot( name="test_callbacks_called_multiple_times_and_out_of_order" diff --git a/tests/integration/devtools/test_hot_reload.py b/tests/integration/devtools/test_hot_reload.py index 83669ba6c3..27047f7f5a 100644 --- a/tests/integration/devtools/test_hot_reload.py +++ b/tests/integration/devtools/test_hot_reload.py @@ -20,9 +20,7 @@ def replace_file(filename, new_content): - path = os.path.join( - os.path.dirname(__file__), "hr_assets", filename - ) + path = os.path.join(os.path.dirname(__file__), "hr_assets", filename) with open(path, "r+") as fp: sleep(1) # ensure a new mod time old_content = fp.read() @@ -92,7 +90,7 @@ def new_text(n): try: until( lambda: dash_duo.driver.execute_script("return window.cheese") == "gouda", - timeout=3 + timeout=3, ) finally: sleep(1) # ensure a new mod time @@ -101,7 +99,7 @@ def new_text(n): until( lambda: dash_duo.driver.execute_script("return window.cheese") == "roquefort", - timeout=3 + timeout=3, ) # we've done a hard reload so someVar is gone diff --git a/tests/integration/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index 52f0e7e223..b207689e1b 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 == 0 + assert dash_duo.redux_state_rqs assert dash_duo.get_logs() == [] diff --git a/tests/integration/renderer/test_due_diligence.py b/tests/integration/renderer/test_due_diligence.py index 4b7c7c297e..de51b05f77 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 == 0, "no callback => no pendingCallbacks" + assert dash_duo.redux_state_rqs, "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 cf2749aa5e..6c726d0def 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -490,18 +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( + ready = self.driver.execute_script( """ - var loadingMap = window.store.getState().loadingMap; - if (!loadingMap || !loadingMap.__dashprivate__idprop__) { - return 0; - } - - return loadingMap.__dashprivate__idprop__.length; + return window.store.getState().isLoading; """ ) - assert pending_count == 0 + assert ready def test_callbacks_with_shared_grandparent(self): app = Dash() From 5e44b226a22a6da1d4f502fabb61e265e41b8277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 May 2020 19:37:15 -0400 Subject: [PATCH 43/90] ready => !isLoading --- tests/integration/test_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index 6c726d0def..1a79090323 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -492,7 +492,7 @@ def update_output(n_clicks): self.wait_for_text_to_equal("#output2", "3") ready = self.driver.execute_script( """ - return window.store.getState().isLoading; + return !window.store.getState().isLoading; """ ) From f5698b0b0a50dfb8975ba669d3a09e9bec77acf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 May 2020 14:19:27 -0400 Subject: [PATCH 44/90] Rework TreeContainer (no connect + provider/consumer) --- dash-renderer/src/APIController.react.js | 40 ++++- dash-renderer/src/TreeContainer.js | 185 ++++++++--------------- dash-renderer/src/checkPropTypes.js | 2 +- dash-renderer/src/utils/TreeContainer.ts | 80 ++++++++++ 4 files changed, 174 insertions(+), 133 deletions(-) create mode 100644 dash-renderer/src/utils/TreeContainer.ts diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 23e5f34c01..a2334401cc 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 @@ -45,10 +48,13 @@ const UnconnectedContainer = props => { const { appLifecycle, + config, dependenciesRequest, + dispatch, + graphs, layoutRequest, layout, - config, + loadingMap, } = props; let content; @@ -65,11 +71,31 @@ const UnconnectedContainer = props => { content =
Error loading dependencies
; } else if (appLifecycle === getAppState('HYDRATED')) { renderedTree.current = true; + content = ( - + + + ); } else { content =
Loading...
; @@ -157,6 +183,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 +196,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/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 0a5322e20a..e206bda92c 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -1,21 +1,17 @@ -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, - find, - has, isEmpty, isNil, keys, map, mergeRight, - path, pick, pickBy, propOr, @@ -27,46 +23,12 @@ 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'; function CheckedComponent(p) { const {element, extraProps, props, children, type} = p; @@ -101,13 +63,45 @@ function createElement(element, props, extraProps, children) { return React.createElement(element, allProps, children); } -class TreeContainer extends Component { +const TreeContainer = memo(props => ( + + {value => } + +)); + +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, @@ -162,13 +156,18 @@ 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) { @@ -216,15 +215,6 @@ class TreeContainer extends Component { ); } - shouldComponentUpdate(nextProps) { - const {_dashprivate_layout, _dashprivate_loadingState} = nextProps; - return ( - _dashprivate_layout !== this.props._dashprivate_layout || - _dashprivate_loadingState.is_loading !== - this.props._dashprivate_loadingState.is_loading - ); - } - getLayoutProps() { return propOr({}, 'props', this.props._dashprivate_layout); } @@ -253,78 +243,21 @@ class TreeContainer extends Component { } TreeContainer.propTypes = { - _dashprivate_graphs: PropTypes.any, - _dashprivate_dispatch: PropTypes.func, _dashprivate_layout: PropTypes.object, _dashprivate_loadingState: PropTypes.object, - _dashprivate_config: PropTypes.object, + _dashprivate_loadingStateHash: PropTypes.string, _dashprivate_path: PropTypes.array, }; -function isLoadingComponent(layout) { - validateComponent(layout); - return Registry.resolve(layout)._dashprivate_isLoadingComponent; -} - -function getLoadingState(componentLayout, componentPath, loadingMap) { - if (isNil(loadingMap)) { - return { - is_loading: false, - }; - } - - const loadingFragment = path(componentPath, loadingMap); - // Component and children are not loading if there's no loading fragment - // for the component's path in the layout. - if (isNil(loadingFragment)) { - return { - is_loading: false, - }; - } - - const {__dashprivate__idprop__: ids} = loadingFragment; - - if (isLoadingComponent(componentLayout)) { - return { - is_loading: true, - prop_name: ids[0].property, - component_name: ids[0].id, - }; - } - - const entry = find(id => id.id === componentLayout.props.id, ids ?? []); - if (entry) { - return { - is_loading: true, - prop_name: entry.property, - component_name: entry.id, - }; - } - - return { - is_loading: false, - }; -} - -export const AugmentedTreeContainer = connect( - state => ({ - graphs: state.graphs, - loadingMap: state.loadingMap, - 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, - ownProps._dashprivate_path, - stateProps.loadingMap - ), - _dashprivate_config: stateProps.config, - }) -)(TreeContainer); +BaseTreeContainer.propTypes = { + _dashprivate_config: PropTypes.object, + _dashprivate_dispatch: PropTypes.func, + _dashprivate_graphs: PropTypes.any, + _dashprivate_layout: PropTypes.object, + _dashprivate_loadingState: PropTypes.object, + _dashprivate_loadingStateHash: PropTypes.string, + _dashprivate_loadingMap: PropTypes.any, + _dashprivate_path: PropTypes.array, +}; -export default AugmentedTreeContainer; +export default TreeContainer; 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/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts new file mode 100644 index 0000000000..79cc8ddfe1 --- /dev/null +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -0,0 +1,80 @@ +import { path, isNil, find, type, has } from "ramda"; + +import Registry from '../registry'; + +function isLoadingComponent(layout: any) { + validateComponent(layout); + return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; +} + +const NULL_LOADING_STATE = { + is_loading: false +}; + +export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) { + if (isNil(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 (isNil(loadingFragment)) { + return NULL_LOADING_STATE; + } + + const ids: any[] = loadingFragment.__dashprivate__idprop__; + + if (isLoadingComponent(componentLayout)) { + return { + is_loading: true, + prop_name: ids[0].property, + component_name: ids[0].id, + }; + } + + const entry = find(id => id.id === componentLayout.props.id, ids ?? []); + if (entry) { + return { + is_loading: true, + prop_name: entry.property, + component_name: entry.id, + }; + } + + return NULL_LOADING_STATE; +} + +export const getLoadingHash = ( + componentPath: any, + loadingMap: any +) => ( + ((loadingMap && (path(componentPath, loadingMap) as any)?.__dashprivate__idprop__) ?? []) 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) + ); + } +} \ No newline at end of file From b624cd9810294995cf31b1deba6277735d3d593b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 May 2020 14:39:17 -0400 Subject: [PATCH 45/90] remove console.log --- dash-renderer/src/TreeContainer.js | 7 ++---- dash-renderer/src/actions/dependencies_ts.ts | 11 +------- .../src/observers/executedCallbacks.ts | 4 --- .../src/observers/executingCallbacks.ts | 14 +++-------- dash-renderer/src/observers/loadingMap.ts | 3 --- .../src/observers/prioritizedCallbacks.ts | 2 -- .../src/observers/requestedCallbacks.ts | 25 +------------------ .../src/observers/storedCallbacks.ts | 4 --- 8 files changed, 7 insertions(+), 63 deletions(-) diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index e206bda92c..bb392e445a 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -65,7 +65,7 @@ function createElement(element, props, extraProps, children) { const TreeContainer = memo(props => ( - {value => } + {context => } )); @@ -250,14 +250,11 @@ TreeContainer.propTypes = { }; BaseTreeContainer.propTypes = { + ...TreeContainer.propTypes, _dashprivate_config: PropTypes.object, _dashprivate_dispatch: PropTypes.func, _dashprivate_graphs: PropTypes.any, - _dashprivate_layout: PropTypes.object, - _dashprivate_loadingState: PropTypes.object, - _dashprivate_loadingStateHash: PropTypes.string, _dashprivate_loadingMap: PropTypes.any, - _dashprivate_path: PropTypes.array, }; export default TreeContainer; diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index ca22926fce..acec785455 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -99,18 +99,9 @@ export const getLayoutCallbacks = ( layout, options ); - console.log('SPECIAL', '[getLayoutCallbacks-initial]', callbacks); - - // /* - // Basic implementation - retrieve all `ready` callbacks. - // Follow up callbacks will be triggered by executed callbacks. - // */ - // return getReadyCallbacks(callbacks); /* - This loop is for backward compatibility with previous implementation - of the callbacks chain. Remove from the initial callbacks those that are left - with only excluded inputs. + Remove from the initial callbacks those that are left with only excluded inputs. Exclusion of inputs happens when: - an input is missing diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index 9680754e0b..f17da54211 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -86,8 +86,6 @@ const observer: IStoreObserverDefinition = { return props; } - console.log('onCallbacksChanged.executed', executed); - let requestedCallbacks: ICallback[] = []; let storedCallbacks: IStoredCallback[] = []; @@ -105,7 +103,6 @@ const observer: IStoreObserverDefinition = { } const { data, error, payload } = executionResult; - console.log('onCallbacksChanged.executed', '[executionResult]', cb, data); if (data !== undefined) { forEach(([id, props]: [any, { [key: string]: any }]) => { @@ -208,7 +205,6 @@ const observer: IStoreObserverDefinition = { } }, executed); - console.log('SPECIAL', '[requestedCallbacks]', requestedCallbacks); dispatch(aggregateCallbacks([ executed.length ? removeExecutedCallbacks(executed) : null, executed.length ? addCompletedCallbacks(executed.length) : null, diff --git a/dash-renderer/src/observers/executingCallbacks.ts b/dash-renderer/src/observers/executingCallbacks.ts index 5d23d59cc7..76f93ac4ce 100644 --- a/dash-renderer/src/observers/executingCallbacks.ts +++ b/dash-renderer/src/observers/executingCallbacks.ts @@ -29,8 +29,6 @@ const observer: IStoreObserverDefinition = { }, } = getState(); - console.log('onCallbacksChanged.executing', executing); - const [deferred, skippedOrReady] = partition(cb => cb.executionPromise instanceof Promise, executing); dispatch(aggregateCallbacks([ @@ -42,22 +40,16 @@ const observer: IStoreObserverDefinition = { deferred.forEach(async function (cb: IExecutingCallback) { const result = await cb.executionPromise; - /* - Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled - */ + // Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled const { callbacks: { watched } } = getState(); - /* - Find the callback instance or one that matches its promise (eg. could have been pruned) - */ + // 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` - */ + // Otherwise move to `executed` and remove from `watched` dispatch(aggregateCallbacks([ removeWatchedCallbacks([currentCb]), addExecutedCallbacks([{ diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index 78d304094e..ab121a285d 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -26,9 +26,6 @@ const observer: IStoreObserverDefinition = { paths } = getState(); - const callbacks = [...executing, ...watched, ...executed]; - console.log('onCallbacksChanged.loadingMap', callbacks); - /* Get the path of all components impacted by callbacks with states: executing, watched, executed diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index a1d747388b..9dbc10823d 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -40,8 +40,6 @@ const observer: IStoreObserverDefinition = { const { callbacks: { executing, watched }, config, hooks, layout, paths } = getState(); let { callbacks: { prioritized } } = getState(); - console.log('onCallbacksChanged.prioritized', prioritized); - const available = Math.max( 0, 6 - executing.length - watched.length diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 533c4b86ad..50e3f9202b 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -49,13 +49,11 @@ const observer: IStoreObserverDefinition = { dispatch, getState }) => { - const { callbacks, callbacks: { prioritized, executing, watched, executed, completed, stored }, paths } = getState(); + const { callbacks, callbacks: { prioritized, executing, watched, executed, stored }, paths } = getState(); let { callbacks: { requested } } = getState(); const pendingCallbacks = getPendingCallbacks(callbacks); - console.log('onCallbacksChanged.requested', requested, completed, callbacks); - /* 1. Remove duplicated `requested` callbacks */ @@ -66,7 +64,6 @@ const observer: IStoreObserverDefinition = { */ const rDuplicates = flatten(map( group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -92,7 +89,6 @@ const observer: IStoreObserverDefinition = { */ const pDuplicates = flatten(map( group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -103,7 +99,6 @@ const observer: IStoreObserverDefinition = { const eDuplicates = flatten(map( group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -114,7 +109,6 @@ const observer: IStoreObserverDefinition = { const wDuplicates = flatten(map( group => group.slice(0, -1), - // group => filter(cb => !cb.executionGroup, group).slice(1), values( groupBy( getUniqueIdentifier, @@ -123,10 +117,6 @@ const observer: IStoreObserverDefinition = { ) )) as IExecutingCallback[]; - if (rDuplicates.length || pDuplicates.length || eDuplicates.length || wDuplicates.length) { - console.log('onCallbacksChanged.requested', '[duplicates]', rDuplicates.length, pDuplicates.length, eDuplicates.length, wDuplicates.length); - } - /* 3. Modify or remove callbacks that are outputing to non-existing layout `id`. */ @@ -136,10 +126,6 @@ const observer: IStoreObserverDefinition = { const { added: eAdded, removed: eRemoved } = pruneCallbacks(executing, paths); const { added: wAdded, removed: wRemoved } = pruneCallbacks(watched, paths); - if (rRemoved.length + pRemoved.length + eRemoved.length + wRemoved.length) { - console.log('onCallbacksChanged.requested', '[pruned]', rRemoved.length, pRemoved.length, eRemoved.length, wRemoved.length); - } - /* TODO? Clean up the `requested` list - during the dispatch phase, @@ -157,7 +143,6 @@ const observer: IStoreObserverDefinition = { 4. Find `requested` callbacks that do not depend on a outstanding output (as either input or state) */ let readyCallbacks = getReadyCallbacks(requested, pendingCallbacks); - console.log('onCallbacksChanged.requested', '[readyCallbacks]', readyCallbacks); /* If: @@ -184,10 +169,6 @@ const observer: IStoreObserverDefinition = { cb => cb.executionGroup as any, filter(cb => !isNil(cb.executionGroup), stored) ); - console.log('onCallbacksChanged.requested', '[pendingGroups]', pendingGroups, map(pg => flatten(map( - gcb => gcb.executionMeta.updatedProps, - pg - )), values(pendingGroups))); const dropped: ICallback[] = filter(cb => { if (!cb.executionGroup || !pendingGroups[cb.executionGroup] || !pendingGroups[cb.executionGroup].length) { @@ -220,15 +201,11 @@ const observer: IStoreObserverDefinition = { cb.callback.inputs ); - console.log('SPECIAL', cb, res, inputs, allProps, updated); - return res; }, readyCallbacks ); - console.log('onCallbacksChanged.requested', '[dropped]', readyCallbacks, dropped, pendingGroups); - /* TODO? Clean up the `requested` list - during the dispatch phase, diff --git a/dash-renderer/src/observers/storedCallbacks.ts b/dash-renderer/src/observers/storedCallbacks.ts index 25d5589190..a02301139a 100644 --- a/dash-renderer/src/observers/storedCallbacks.ts +++ b/dash-renderer/src/observers/storedCallbacks.ts @@ -33,8 +33,6 @@ const observer: IStoreObserverDefinition = { let { callbacks: { stored } } = getState(); - console.log('onCallbacksChanged.stored', stored); - const [nullGroupCallbacks, groupCallbacks] = partition( cb => isNil(cb.executionGroup), stored @@ -60,8 +58,6 @@ const observer: IStoreObserverDefinition = { toPairs(executionGroups) ); - console.log('onCallbacksChanged.stored', '[dropped]', nullGroupCallbacks, dropped); - dispatch(aggregateCallbacks([ nullGroupCallbacks.length ? removeStoredCallbacks(nullGroupCallbacks) : null, dropped.length ? removeStoredCallbacks(dropped) : null From 78b1a26e10356435a1dd5ba456ca31c1b36dbea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 May 2020 22:33:16 -0400 Subject: [PATCH 46/90] prevent some intermediary renders --- dash-renderer/src/APIController.react.js | 43 +++++++++++++----------- dash-renderer/src/TreeContainer.js | 23 +++++++++---- dash-renderer/src/utils/TreeContainer.ts | 4 +-- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index a2334401cc..7e8df8cc17 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -29,6 +29,15 @@ export const DashContext = createContext({}); * @returns {*} component */ const UnconnectedContainer = props => { + const { + appLifecycle, + config, + dependenciesRequest, + layoutRequest, + layout, + loadingMap, + } = props; + const [errorLoading, setErrorLoading] = useState(false); const events = useRef(null); @@ -37,6 +46,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(() => { @@ -46,17 +67,6 @@ const UnconnectedContainer = props => { } }); - const { - appLifecycle, - config, - dependenciesRequest, - dispatch, - graphs, - layoutRequest, - layout, - loadingMap, - } = props; - let content; if ( layoutRequest.status && @@ -73,14 +83,7 @@ const UnconnectedContainer = props => { renderedTree.current = true; content = ( - + { [], loadingMap )} - _dashprivate_path={[]} + _dashprivate_path={JSON.stringify([])} /> ); diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index bb392e445a..8fabfd7bc7 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -63,11 +63,20 @@ function createElement(element, props, extraProps, children) { return React.createElement(element, allProps, children); } -const TreeContainer = memo(props => ( - - {context => } - -)); +const TreeContainer = memo( + props => + Boolean() || ( + + {context => ( + + )} + + ) +); class BaseTreeContainer extends Component { constructor(props) { @@ -97,7 +106,7 @@ class BaseTreeContainer extends Component { path, props._dashprivate_loadingMap )} - _dashprivate_path={path} + _dashprivate_path={JSON.stringify(path)} /> ); } @@ -246,7 +255,7 @@ TreeContainer.propTypes = { _dashprivate_layout: PropTypes.object, _dashprivate_loadingState: PropTypes.object, _dashprivate_loadingStateHash: PropTypes.string, - _dashprivate_path: PropTypes.array, + _dashprivate_path: PropTypes.string, }; BaseTreeContainer.propTypes = { diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index 79cc8ddfe1..aacd39123b 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -7,9 +7,7 @@ function isLoadingComponent(layout: any) { return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; } -const NULL_LOADING_STATE = { - is_loading: false -}; +const NULL_LOADING_STATE = false; export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) { if (isNil(loadingMap)) { From b0f117874a6fb0dd98374d1b0cce7912134690fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 May 2020 23:58:34 -0400 Subject: [PATCH 47/90] fix regression on loading_state --- dash-renderer/src/TreeContainer.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 8fabfd7bc7..05312c5238 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -30,6 +30,10 @@ import { } from './utils/TreeContainer'; import {DashContext} from './APIController.react'; +const NOT_LOADING = { + is_loading: false, +}; + function CheckedComponent(p) { const {element, extraProps, props, children, type} = p; @@ -201,7 +205,10 @@ class BaseTreeContainer 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 ( Date: Fri, 22 May 2020 00:58:25 -0400 Subject: [PATCH 48/90] - unconnect ComponentErrorBoundary --- dash-renderer/src/APIController.react.js | 2 ++ dash-renderer/src/TreeContainer.js | 10 +++++++++- .../error/ComponentErrorBoundary.react.js | 15 ++------------- dash-renderer/src/utils/TreeContainer.ts | 4 +++- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 7e8df8cc17..6b45d2248b 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -33,6 +33,7 @@ const UnconnectedContainer = props => { appLifecycle, config, dependenciesRequest, + error, layoutRequest, layout, loadingMap, @@ -85,6 +86,7 @@ const UnconnectedContainer = props => { content = ( {_dashprivate_config.props_check ? ( ({ - error: state.error, - }), - dispatch => { - return {dispatch}; - } -)(Radium(UnconnectedComponentErrorBoundary)); - export default ComponentErrorBoundary; diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index aacd39123b..79cc8ddfe1 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -7,7 +7,9 @@ function isLoadingComponent(layout: any) { return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; } -const NULL_LOADING_STATE = false; +const NULL_LOADING_STATE = { + is_loading: false +}; export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) { if (isNil(loadingMap)) { From ff18d80e76328e9188910700f441d53856b6d97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 12:44:18 -0400 Subject: [PATCH 49/90] fix hash generation --- dash-renderer/src/APIController.react.js | 1 - dash-renderer/src/TreeContainer.js | 26 ++++++++++-------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/dash-renderer/src/APIController.react.js b/dash-renderer/src/APIController.react.js index 6b45d2248b..8479b0fd07 100644 --- a/dash-renderer/src/APIController.react.js +++ b/dash-renderer/src/APIController.react.js @@ -94,7 +94,6 @@ const UnconnectedContainer = props => { loadingMap )} _dashprivate_loadingStateHash={getLoadingHash( - layout, [], loadingMap )} diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 5ce12b7421..bf317ad17d 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -67,20 +67,17 @@ function createElement(element, props, extraProps, children) { return React.createElement(element, allProps, children); } -const TreeContainer = memo( - props => - Boolean() || ( - - {context => ( - - )} - - ) -); +const TreeContainer = memo(props => ( + + {context => ( + + )} + +)); class BaseTreeContainer extends Component { constructor(props) { @@ -107,7 +104,6 @@ class BaseTreeContainer extends Component { props._dashprivate_loadingMap )} _dashprivate_loadingStateHash={getLoadingHash( - component, path, props._dashprivate_loadingMap )} From 9ccc017d69fef5f2e1337100fc26292b6bc45605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 15:48:38 -0400 Subject: [PATCH 50/90] remove dead code --- dash-renderer/src/actions/dependencies_ts.ts | 35 -------------------- 1 file changed, 35 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index acec785455..d8d68e1bfe 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -27,41 +27,6 @@ export const combineIdAndProp = ({ property }: ICallbackProperty) => `${stringifyId(id)}.${property}`; -// /* -// * 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 const followForward = ( -// graphs: any, -// paths: any, -// callbacks: ICallback[] -// ): ICallback[] => { -// callbacks = callbacks.slice(0); -// let i; -// let callback: ICallback; - -// const followOutput = ({ id, property }: ICallbackProperty) => { -// callbacks = concat(callbacks, getCallbacksByInput( -// graphs, -// paths, -// id, -// property, -// INDIRECT -// )); -// }; - -// // 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; -// } - export const getReadyCallbacks = ( candidates: ICallback[], callbacks: ICallback[] = candidates From 6a4f98f1eb0327da17dde49566d170789b02debe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 15:53:44 -0400 Subject: [PATCH 51/90] Use `false` instead of `{ is_loading: false}` --- dash-renderer/src/utils/TreeContainer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index 79cc8ddfe1..aacd39123b 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -7,9 +7,7 @@ function isLoadingComponent(layout: any) { return (Registry.resolve(layout) as any)._dashprivate_isLoadingComponent; } -const NULL_LOADING_STATE = { - is_loading: false -}; +const NULL_LOADING_STATE = false; export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) { if (isNil(loadingMap)) { From bed05230b6701c2555c0c098428283e9c0ac2573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 16:10:32 -0400 Subject: [PATCH 52/90] share (id,prop) instance for all paths --- dash-renderer/src/observers/loadingMap.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index ab121a285d..7901a032b0 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -28,7 +28,10 @@ const observer: IStoreObserverDefinition = { /* Get the path of all components impacted by callbacks - with states: executing, watched, executed + 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 = flatten(map( @@ -41,11 +44,13 @@ const observer: IStoreObserverDefinition = { reduce( (res, path) => { let target = res; - target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; - target.__dashprivate__idprop__.push({ + const idprop = { id: path.id, property: path.property - }); + }; + + target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; + target.__dashprivate__idprop__.push(idprop); forEach(p => { target = (target[p] = @@ -54,11 +59,7 @@ const observer: IStoreObserverDefinition = { ) target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; - target.__dashprivate__idprop__.push({ - id: path.id, - property: path.property - }); - + target.__dashprivate__idprop__.push(idprop); }, path.path); return res; From 1105ca6912f467b68f44baf2380c6fd1bb29c8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 16:23:19 -0400 Subject: [PATCH 53/90] small loadingMap observer optimizations --- dash-renderer/src/observers/loadingMap.ts | 11 ++++++---- dash-renderer/src/utils/TreeContainer.ts | 25 +++++++++++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index 7901a032b0..0b6305f9ad 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -49,8 +49,11 @@ const observer: IStoreObserverDefinition = { property: path.property }; - target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; - target.__dashprivate__idprop__.push(idprop); + // Assign one affected prop for this path + target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || idprop; + // 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] = @@ -58,8 +61,8 @@ const observer: IStoreObserverDefinition = { p === 'children' ? [] : {} ) - target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || []; - target.__dashprivate__idprop__.push(idprop); + target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || []; + target.__dashprivate__idprops__.push(idprop); }, path.path); return res; diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index aacd39123b..65b2c5c349 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -1,4 +1,4 @@ -import { path, isNil, find, type, has } from "ramda"; +import { path, type, has } from "ramda"; import Registry from '../registry'; @@ -10,33 +10,32 @@ function isLoadingComponent(layout: any) { const NULL_LOADING_STATE = false; export function getLoadingState(componentLayout: any, componentPath: any, loadingMap: any) { - if (isNil(loadingMap)) { + 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 (isNil(loadingFragment)) { + if (!loadingFragment) { return NULL_LOADING_STATE; } - const ids: any[] = loadingFragment.__dashprivate__idprop__; - - if (isLoadingComponent(componentLayout)) { + const idprop: any = loadingFragment.__dashprivate__idprop__; + if (idprop) { return { is_loading: true, - prop_name: ids[0].property, - component_name: ids[0].id, + prop_name: idprop.property, + component_name: idprop.id, }; } - const entry = find(id => id.id === componentLayout.props.id, ids ?? []); - if (entry) { + const idprops: any = loadingFragment.__dashprivate__idprops__?.[0]; + if (idprops && isLoadingComponent(componentLayout)) { return { is_loading: true, - prop_name: entry.property, - component_name: entry.id, + prop_name: idprops.property, + component_name: idprops.id, }; } @@ -47,7 +46,7 @@ export const getLoadingHash = ( componentPath: any, loadingMap: any ) => ( - ((loadingMap && (path(componentPath, loadingMap) as any)?.__dashprivate__idprop__) ?? []) as any[] + ((loadingMap && (path(componentPath, loadingMap) as any)?.__dashprivate__idprops__) ?? []) as any[] ).map(({ id, property }) => `${id}.${property}`).join(','); export function validateComponent(componentDefinition: any) { From b61216a81cc34229a8504dbe7b5155352fed1108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 16:37:05 -0400 Subject: [PATCH 54/90] improve requestedCallbacks observer comments --- dash-renderer/src/observers/executedCallbacks.ts | 5 +++-- dash-renderer/src/observers/requestedCallbacks.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index f17da54211..24bb9e3907 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -166,8 +166,9 @@ const observer: IStoreObserverDefinition = { } }, 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: { diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 50e3f9202b..b8dbda7a46 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -55,7 +55,7 @@ const observer: IStoreObserverDefinition = { const pendingCallbacks = getPendingCallbacks(callbacks); /* - 1. Remove duplicated `requested` callbacks + 1. Remove duplicated `requested` callbacks - give precedence to newer callbacks over older ones */ /* @@ -165,28 +165,40 @@ const observer: IStoreObserverDefinition = { /* 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, From 4e2f0b4ca0f852b8156235d928cd79e75a73be69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 16:55:43 -0400 Subject: [PATCH 55/90] stringifyId for loading_state --- dash-renderer/src/utils/TreeContainer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index 65b2c5c349..67371fe459 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -1,6 +1,7 @@ import { path, type, has } from "ramda"; import Registry from '../registry'; +import { stringifyId } from "../actions/dependencies"; function isLoadingComponent(layout: any) { validateComponent(layout); @@ -26,7 +27,7 @@ export function getLoadingState(componentLayout: any, componentPath: any, loadin return { is_loading: true, prop_name: idprop.property, - component_name: idprop.id, + component_name: stringifyId(idprop.id), }; } @@ -35,7 +36,7 @@ export function getLoadingState(componentLayout: any, componentPath: any, loadin return { is_loading: true, prop_name: idprops.property, - component_name: idprops.id, + component_name: stringifyId(idprops.id), }; } From d1ab912ae1bee0dfd9710cfb6852953d6b5738c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 17:42:52 -0400 Subject: [PATCH 56/90] update comments --- dash-renderer/src/observers/executedCallbacks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index 24bb9e3907..1bdfc9baae 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -112,8 +112,7 @@ const observer: IStoreObserverDefinition = { // Components will trigger callbacks on their own as required (eg. derived) const appliedProps = applyProps(parsedId, props); - // Skip prop-triggered callbacks for callbacks with an execution group - these callbacks - // should already be present in `requested` + // Add callbacks for modified inputs requestedCallbacks = concat( requestedCallbacks, flatten(map( @@ -132,6 +131,7 @@ const observer: IStoreObserverDefinition = { const paths = computePaths(children, oldChildrenPath, oldPaths); dispatch(setPaths(paths)); + // Get callbacks for new layout (w/ execution group) requestedCallbacks = concat( requestedCallbacks, getLayoutCallbacks(graphs, paths, children, { From 00e5e6eee3c9b1e655763eec982f0bcd72b5e756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 May 2020 17:48:13 -0400 Subject: [PATCH 57/90] bump maximum concurrent callbacks to 10 --- dash-renderer/src/observers/prioritizedCallbacks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 9dbc10823d..0e6c1c3c26 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -42,7 +42,7 @@ const observer: IStoreObserverDefinition = { const available = Math.max( 0, - 6 - executing.length - watched.length + 10 - executing.length - watched.length ); prioritized = prioritized.slice(0, available); From 5905631d23a1b01c5fdeb20c767df33e6e6156db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 May 2020 17:45:40 -0400 Subject: [PATCH 58/90] Prioitize callbacks based on depth/breadth of subsequent callback chain --- dash-renderer/src/actions/dependencies.js | 70 ++++++--------- dash-renderer/src/actions/dependencies_ts.ts | 86 ++++++++++++++++++- .../src/observers/executedCallbacks.ts | 3 +- .../src/observers/prioritizedCallbacks.ts | 28 +++++- dash-renderer/src/types/callbacks.ts | 6 +- 5 files changed, 139 insertions(+), 54 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 1809d5c2dc..db05510385 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -26,7 +26,13 @@ import { zipObj, } from 'ramda'; -import {combineIdAndProp, DIRECT, INDIRECT, mergeMax} from './dependencies_ts'; +import { + combineIdAndProp, + getCallbacksByInput, + INDIRECT, + mergeMax, + getPriority, +} from './dependencies_ts'; import {computePaths, getPath} from './paths'; import {crawlLayout} from './utils'; @@ -834,7 +840,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]; @@ -889,7 +902,7 @@ function getAnyVals(patternVals, vals) { return matches.length ? JSON.stringify(matches) : ''; } -function resolveDeps(refKeys, refVals, refPatternVals) { +export function resolveDeps(refKeys, refVals, refPatternVals) { return paths => ({id: idPattern, property}) => { if (typeof idPattern === 'string') { const path = getPath(paths, idPattern); @@ -1032,7 +1045,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) { @@ -1089,47 +1102,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 []; @@ -1285,5 +1257,11 @@ export function getCallbacksInLayout(graphs, paths, layoutChunk, opts) { } }); - return callbacks; + return map( + cb => ({ + ...cb, + priority: getPriority(graphs, paths, cb), + }), + callbacks + ); } diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index d8d68e1bfe..12b464a0d3 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -12,10 +12,11 @@ import { mergeWith, partition, pickBy, + props, reduce } from 'ramda'; import { ICallback, ICallbackProperty } from '../types/callbacks'; -import { getCallbacksByInput, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued } from './dependencies'; +import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued, resolveDeps, idMatch } from './dependencies'; import { getPath } from './paths'; export const DIRECT = 2; @@ -27,6 +28,89 @@ export const combineIdAndProp = ({ property }: ICallbackProperty) => `${stringifyId(id)}.${property}`; +export function getCallbacksByInput( + graphs: any, + paths: any, + id: any, + prop: any, + changeType?: any +): 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; + match.priority = getPriority(graphs, paths, match) + }); + return matches; +} + +/* + * 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 getPriority(graphs: any, paths: any, callback: ICallback) { + let callbacks: ICallback[] = [callback]; + let priority: number[] = []; + + while (callbacks.length) { + const outputs = flatten(map( + cb => flatten(cb.getOutputs(paths)), + callbacks + )); + + callbacks = flatten(map( + ({ id, property }: any) => getCallbacksByInput( + graphs, + paths, + id, + property, + INDIRECT + ), + outputs + )); + + if (callbacks.length) { + priority.push(callbacks.length); + } + } + + return priority; +} + export const getReadyCallbacks = ( candidates: ICallback[], callbacks: ICallback[] = candidates diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index 1bdfc9baae..96f07bb6bd 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -22,10 +22,11 @@ import { addStoredCallbacks } from '../actions/callbacks'; -import { parseIfWildcard, getCallbacksByInput } from '../actions/dependencies'; +import { parseIfWildcard } from '../actions/dependencies'; import { combineIdAndProp, + getCallbacksByInput, getLayoutCallbacks, includeObservers } from '../actions/dependencies_ts'; diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 0e6c1c3c26..d9eb34a6d6 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -1,11 +1,12 @@ import { flatten, + includes, map, - reduce, - uniq, - pluck, partition, - includes + pluck, + reduce, + sort, + uniq } from 'ramda'; import { IStoreState } from "../store"; @@ -32,6 +33,22 @@ import { } from '../types/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; +const sortPriority = (c1: ICallback, c2: ICallback): number => { + const lDiff = c2.priority.length - c1.priority.length; + if (lDiff) { + return lDiff; + } + + for (let i = 0; i < c1.priority.length; ++i) { + const pDiff = c2.priority[i] - c1.priority[i]; + if (pDiff) { + return pDiff; + } + } + + return 0; +} + const observer: IStoreObserverDefinition = { observer: async ({ dispatch, @@ -45,6 +62,9 @@ const observer: IStoreObserverDefinition = { 10 - executing.length - watched.length ); + // Order prioritized callbacks based on depth and breadth of callback chain + prioritized = sort(sortPriority, prioritized); + prioritized = prioritized.slice(0, available); if (!prioritized.length) { return; diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index eb36c8e14e..5a64573ca0 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -22,13 +22,15 @@ export interface ICallback { outputs: ICallbackProperty[]; state: ICallbackProperty[]; }; + changedPropIds: any; executionGroup?: string; + priority: number[]; getInputs: (paths: any) => ILayoutCallbackProperty[]; getOutputs: (paths: any) => ILayoutCallbackProperty[]; getState: (paths: any) => ILayoutCallbackProperty[]; prevent_initial_call: boolean; - - [key: string]: any; + requestedOutputs: object; + resolvedId: any; } export interface IExecutingCallback extends ICallback { From df854dde5c735ed624b673172f1b26c4615b094e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 May 2020 08:54:56 -0400 Subject: [PATCH 59/90] fix broken `getCallbacksByInput` import --- dash-renderer/src/reducers/reducer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/src/reducers/reducer.js b/dash-renderer/src/reducers/reducer.js index 3fd04113df..d238b75225 100644 --- a/dash-renderer/src/reducers/reducer.js +++ b/dash-renderer/src/reducers/reducer.js @@ -1,7 +1,7 @@ import {forEach, isEmpty, keys, path} from 'ramda'; import {combineReducers} from 'redux'; -import {getCallbacksByInput} from '../actions/dependencies'; +import {getCallbacksByInput} from '../actions/dependencies_ts'; import createApiReducer from './api'; import appLifecycle from './appLifecycle'; From 280c7b5f85c78eef91c7b1427bc22ca1f23d9c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 May 2020 15:33:14 -0400 Subject: [PATCH 60/90] fix dvcv test cases (prevent infinite loop when evaluating priority) --- dash-renderer/src/actions/dependencies_ts.ts | 28 ++++++++++++++----- .../src/observers/executedCallbacks.ts | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 12b464a0d3..457a115aa2 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -33,7 +33,8 @@ export function getCallbacksByInput( paths: any, id: any, prop: any, - changeType?: any + changeType?: any, + withPriority: boolean = true ): ICallback[] { const matches: ICallback[] = []; const idAndProp = combineIdAndProp({ id, property: prop }); @@ -71,7 +72,9 @@ export function getCallbacksByInput( } matches.forEach(match => { match.changedPropIds[idAndProp] = changeType || DIRECT; - match.priority = getPriority(graphs, paths, match) + if (withPriority) { + match.priority = getPriority(graphs, paths, match) + } }); return matches; } @@ -84,13 +87,23 @@ export function getCallbacksByInput( */ export function getPriority(graphs: any, paths: any, callback: ICallback) { let callbacks: ICallback[] = [callback]; + let touchedOutputs: { [key: string]: boolean } = {}; let priority: number[] = []; while (callbacks.length) { - const outputs = flatten(map( - cb => flatten(cb.getOutputs(paths)), - callbacks - )); + 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( @@ -98,7 +111,8 @@ export function getPriority(graphs: any, paths: any, callback: ICallback) { paths, id, property, - INDIRECT + INDIRECT, + false ), outputs )); diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index 96f07bb6bd..dd0bed7f46 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -117,7 +117,7 @@ const observer: IStoreObserverDefinition = { requestedCallbacks = concat( requestedCallbacks, flatten(map( - prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop), + prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop, true), keys(props) )) ); From 720c3503abc3f102fb134c38eda8fe52a8ee06aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 May 2020 16:31:27 -0400 Subject: [PATCH 61/90] fix loading state on target --- dash-renderer/src/observers/loadingMap.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index 0b6305f9ad..728e5738d0 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -49,8 +49,6 @@ const observer: IStoreObserverDefinition = { property: path.property }; - // Assign one affected prop for this path - target.__dashprivate__idprop__ = target.__dashprivate__idprop__ || idprop; // Assign all affected props for this path and nested paths target.__dashprivate__idprops__ = target.__dashprivate__idprops__ || []; target.__dashprivate__idprops__.push(idprop); @@ -65,6 +63,9 @@ const observer: IStoreObserverDefinition = { 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, From 96d4d671420cd141caa24b9de6a5a19e96dc457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 May 2020 21:32:17 -0400 Subject: [PATCH 62/90] optional parameter on `setProps` to force update -- this is useful if props get updated by third-parties for which we don't control timing with respect to the renderer --- dash-renderer/src/TreeContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index bf317ad17d..78c50b178a 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -112,7 +112,7 @@ class BaseTreeContainer extends Component { ); } - setProps(newProps) { + setProps(newProps, forceOnEqual = false) { const { _dashprivate_graphs, _dashprivate_dispatch, @@ -123,7 +123,7 @@ class BaseTreeContainer extends Component { const oldProps = this.getLayoutProps(); const {id} = oldProps; const changedProps = pickBy( - (val, key) => !equals(val, oldProps[key]), + (val, key) => forceOnEqual || !equals(val, oldProps[key]), newProps ); if (!isEmpty(changedProps)) { From 6c07a0d44e182c965a6eeac5aefa669eefedfcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 May 2020 16:31:42 -0400 Subject: [PATCH 63/90] isAppReady - fix regression on ids --- dash-renderer/src/observers/prioritizedCallbacks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index d9eb34a6d6..1046d556f6 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -90,8 +90,8 @@ const observer: IStoreObserverDefinition = { const ids = reduce((res, [cb]) => ([ ...res, - ...cb.getInputs(paths), - ...cb.getState(paths) + ...flatten(cb.getInputs(paths)), + ...flatten(cb.getState(paths)) ]), [] as ICallbackProperty[], callbacks); /* From 854784f9baa073db28db3a90f402614defa62b7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 May 2020 16:50:08 -0400 Subject: [PATCH 64/90] fix docker versions --- .circleci/config.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 39a2b141e4..267b1a8f90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ jobs: artifacts: docker: - - image: circleci/python:3.7-stretch-node-browsers + - image: circleci/python:3.7.6-stretch-node-browsers environment: PYVERSION: python37 steps: @@ -27,7 +27,7 @@ jobs: lint-unit-37: &lint-unit working_directory: ~/dash docker: - - image: circleci/python:3.7-stretch-node-browsers + - image: circleci/python:3.7.6-stretch-node-browsers environment: PYLINTRC: .pylintrc37 PYVERSION: python37 @@ -64,7 +64,7 @@ jobs: lint-unit-36: <<: *lint-unit docker: - - image: circleci/python:3.6-stretch-node-browsers + - image: circleci/python:3.6.9-stretch-node-browsers environment: PYLINTRC: .pylintrc PYVERSION: python36 @@ -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 @@ -80,7 +80,7 @@ jobs: build-core-37: &build-core working_directory: ~/dash docker: - - image: circleci/python:3.7-stretch-node-browsers + - image: circleci/python:3.7.6-stretch-node-browsers environment: PYVERSION: python37 steps: @@ -115,21 +115,21 @@ jobs: build-core-36: <<: *build-core docker: - - image: circleci/python:3.6-stretch-node-browsers + - image: circleci/python:3.6.9-stretch-node-browsers environment: PYVERSION: python36 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 build-misc-37: &build-misc working_directory: ~/dash docker: - - image: circleci/python:3.7-stretch-node-browsers + - image: circleci/python:3.7.6-stretch-node-browsers environment: PYVERSION: python37 @@ -165,14 +165,14 @@ jobs: build-misc-36: <<: *build-misc docker: - - image: circleci/python:3.6-stretch-node-browsers + - image: circleci/python:3.6.9-stretch-node-browsers environment: PYVERSION: python36 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 @@ -240,7 +240,7 @@ jobs: cd dash-html-components; npm ci && npm run build; rm -rf !(.|..|DESCRIPTION|LICENSE.txt|LICENSE|NAMESPACE|.Rbuildignore|R|man|inst|vignettes|build) cd ../dash-core-components; npm ci && npm run build; rm -rf !(.|..|DESCRIPTION|LICENSE.txt|LICENSE|NAMESPACE|.Rbuildignore|R|man|inst|vignettes|build) cd ../dash-table; npm ci && npm run build; rm -rf !(.|..|DESCRIPTION|LICENSE.txt|LICENSE|NAMESPACE|.Rbuildignore|R|man|inst|vignettes|build); cd .. - + - run: name: 🔧fix up dash metadata command: | @@ -286,7 +286,7 @@ jobs: Rscript -e "message(devtools::check_failures(path = '${DHC_CHECK_DIR}'))" Rscript -e "message(devtools::check_failures(path = '${DCC_CHECK_DIR}'))" Rscript -e "message(devtools::check_failures(path = '${DT_CHECK_DIR}'))" - Rscript -e "message(devtools::check_failures(path = '${DASH_CHECK_DIR}'))" + Rscript -e "message(devtools::check_failures(path = '${DASH_CHECK_DIR}'))" # warnings are errors; enable for stricter checks once CRAN submission finished # if grep -q -R "WARNING" "${DHC_CHECK_DIR}/00check.log"; then exit 1; fi # if grep -q -R "WARNING" "${DCC_CHECK_DIR}/00check.log"; then exit 1; fi @@ -317,13 +317,13 @@ jobs: - run: name: 🦔 percy finalize command: npx percy finalize --all - when: on_fail + when: on_fail test-37: &test working_directory: ~/dash docker: - - image: circleci/python:3.7-stretch-node-browsers + - image: circleci/python:3.7.6-stretch-node-browsers environment: PERCY_PARALLEL_TOTAL: -1 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: True @@ -365,7 +365,7 @@ jobs: test-36: <<: *test docker: - - image: circleci/python:3.6-stretch-node-browsers + - image: circleci/python:3.6.9-stretch-node-browsers environment: PERCY_ENABLE: 0 PYVERSION: python36 @@ -373,7 +373,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 From 983258e2846ae5a727f0c65ad7b2c06b68c611cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 May 2020 09:00:56 -0400 Subject: [PATCH 65/90] improve typing --- dash-renderer/package-lock.json | 14 ++--- dash-renderer/package.json | 2 +- dash-renderer/src/actions/callbacks.ts | 8 +-- dash-renderer/src/actions/dependencies.js | 60 +------------------ dash-renderer/src/actions/dependencies_ts.ts | 61 +++++++++++++++++++- dash-renderer/src/observers/loadingMap.ts | 3 +- dash-renderer/src/types/callbacks.ts | 42 ++++++++------ 7 files changed, 99 insertions(+), 91 deletions(-) diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index e70fb6e82f..cedeada412 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -3324,12 +3324,12 @@ "dev": true }, "@types/ramda": { - "version": "0.26.21", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.26.21.tgz", - "integrity": "sha512-zMYtIZMceA6BvH+or6LmewLBgojbXg5+FGCwjO8K+Z+d/ZWxILmhhASXkehW0PqJL+V0QbyDeeAHix0dvEKXfQ==", + "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": "^3.8.4" + "ts-toolbelt": "^6.3.3" } }, "@types/react": { @@ -18548,9 +18548,9 @@ } }, "ts-toolbelt": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.14.0.tgz", - "integrity": "sha512-DYjD8tL7M1kBogRd9UKg3bUP5yh69WWcMSaA3By8ATiJU9fgYudSYIe8tWD5cpPkrGCdGGnKXQHG+5IrjJ5uhQ==", + "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": { diff --git a/dash-renderer/package.json b/dash-renderer/package.json index 11df48682d..5a8b777f42 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -45,7 +45,7 @@ "@babel/preset-env": "^7.8.7", "@babel/preset-react": "^7.8.3", "@svgr/webpack": "^5.2.0", - "@types/ramda": "^0.26.21", + "@types/ramda": "^0.27.6", "@types/react": "^16.9.34", "@types/react-redux": "^7.1.7", "@types/redux": "^3.6.0", diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 5209aff069..249a38f1c1 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -354,7 +354,7 @@ export function executeCallback( return data; } - function handleServerside(payload: any) { + function handleServerside(payload: any): Promise { if (hooks.request_pre !== null) { hooks.request_pre(payload); } @@ -363,13 +363,13 @@ export function executeCallback( `${urlBase(config)}_dash-update-component`, mergeDeepRight(config.fetch, { method: 'POST', - headers: getCSRFHeader(), + headers: getCSRFHeader() as any, body: JSON.stringify(payload), }) - ).then(res => { + ).then((res: any) => { const { status } = res; if (status === STATUS.OK) { - return res.json().then(data => { + return res.json().then((data: any) => { const { multi, response } = data; if (hooks.request_post !== null) { hooks.request_post(payload, response); diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index db05510385..43ef956645 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -29,9 +29,11 @@ import { import { combineIdAndProp, getCallbacksByInput, + getPriority, INDIRECT, mergeMax, - getPriority, + makeResolvedCallback, + resolveDeps, } from './dependencies_ts'; import {computePaths, getPath} from './paths'; @@ -902,60 +904,6 @@ function getAnyVals(patternVals, vals) { return matches.length ? JSON.stringify(matches) : ''; } -export 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, - requestedOutputs: {}, -}); - /* * Does this item (input / output / state) support multiple values? * string IDs do not; wildcard IDs only do if they contain ALL or ALLSMALLER @@ -978,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. diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 457a115aa2..1eabee9700 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -13,10 +13,11 @@ import { partition, pickBy, props, - reduce + reduce, + zipObj } from 'ramda'; -import { ICallback, ICallbackProperty } from '../types/callbacks'; -import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued, resolveDeps, idMatch } from './dependencies'; +import { ICallback, ICallbackProperty, ICallbackDefinition, ILayoutCallbackProperty, ICallbackTemplate } from '../types/callbacks'; +import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued, idMatch } from './dependencies'; import { getPath } from './paths'; export const DIRECT = 2; @@ -239,6 +240,28 @@ export function includeObservers(id: any, props: any, graphs: any, paths: any): )); } +/* + * 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, + requestedOutputs: {}, +}); + export function pruneCallbacks(callbacks: T[], paths: any): { added: T[], removed: T[] @@ -265,4 +288,36 @@ export function pruneCallbacks(callbacks: T[], paths: any): 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; + }; } \ No newline at end of file diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index 728e5738d0..12a9277e80 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -10,6 +10,7 @@ import { import { setLoadingMap } from '../actions/loadingMap'; import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; +import { ILayoutCallbackProperty } from '../types/callbacks'; const observer: IStoreObserverDefinition = { observer: ({ @@ -34,7 +35,7 @@ const observer: IStoreObserverDefinition = { are impacted for this node and nested nodes. */ - const loadingPaths = flatten(map( + const loadingPaths: ILayoutCallbackProperty[] = flatten(map( cb => cb.getOutputs(paths), [...executing, ...watched, ...executed] )); diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 5a64573ca0..d47d88b459 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -1,5 +1,18 @@ 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; @@ -9,30 +22,23 @@ export interface ILayoutCallbackProperty extends ICallbackProperty { path: (string | number)[]; } -export interface ICallback { +export interface ICallbackTemplate { anyVals: any[] | string; - callback: { - clientside_function?: { - namespace: string; - function_name: string; - }; - input: string; - inputs: ICallbackProperty[]; - output: string; - outputs: ICallbackProperty[]; - state: ICallbackProperty[]; - }; + callback: ICallbackDefinition; changedPropIds: any; executionGroup?: string; - priority: number[]; - getInputs: (paths: any) => ILayoutCallbackProperty[]; - getOutputs: (paths: any) => ILayoutCallbackProperty[]; - getState: (paths: any) => ILayoutCallbackProperty[]; - prevent_initial_call: boolean; - requestedOutputs: object; + initialCall: boolean; + getInputs: (paths: any) => ILayoutCallbackProperty[][]; + getOutputs: (paths: any) => ILayoutCallbackProperty[][]; + getState: (paths: any) => ILayoutCallbackProperty[][]; + requestedOutputs: { [key: string]: any }; resolvedId: any; } +export interface ICallback extends ICallbackTemplate { + priority: number[]; +} + export interface IExecutingCallback extends ICallback { executionPromise: Promise | CallbackResult | null; } From 9d395f0d28d667a6822b170e80d7c2fc982b0fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 May 2020 09:23:46 -0400 Subject: [PATCH 66/90] noise From 0da58103cdc60fdef78880ec96a0dc882752dda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 1 Jun 2020 17:51:42 -0400 Subject: [PATCH 67/90] - typo - declare the store observers alongside the store itself --- dash-renderer/src/AppProvider.react.tsx | 17 +------------- dash-renderer/src/store.ts | 30 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index a26c32dbc7..83cc7ba05e 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -2,25 +2,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import {Provider} from 'react-redux'; -import initializeStore, { observe } from './store'; +import initializeStore from './store'; import AppContainer from './AppContainer.react'; -import executedCallbacks from './observers/executedCallbacks'; -import executingCallbacks from './observers/executingCallbacks'; -import isLoading from './observers/isLoading' -import loadingMap from './observers/loadingMap'; -import prioritizeCallbacks from './observers/prioritizedCallbacks'; -import requestedCallbacks from './observers/requestedCallbacks'; -import storedCallbacks from './observers/storedCallbacks'; - const store = initializeStore(); -observe(isLoading); -observe(loadingMap); -observe(requestedCallbacks); -observe(prioritizeCallbacks); -observe(executingCallbacks); -observe(executedCallbacks); -observe(storedCallbacks); const AppProvider = ({hooks}: any) => { return ( diff --git a/dash-renderer/src/store.ts b/dash-renderer/src/store.ts index bb87a5fdb2..3b26f75a92 100644 --- a/dash-renderer/src/store.ts +++ b/dash-renderer/src/store.ts @@ -1,3 +1,4 @@ +import { once } from 'ramda'; import { createStore, applyMiddleware, Store, Observer } from 'redux'; import thunk from 'redux-thunk'; import {createReducer} from './reducers/reducer'; @@ -6,6 +7,19 @@ 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; @@ -16,16 +30,22 @@ export interface IStoreState { let store: Store; const storeObserver = new StoreObserver(); -export const observe = storeObserver.observe; +const setObservers = once(() => { + const observe = storeObserver.observe; -export interface IStoreObserver { - observer: Observer>; - inputs: string[]; -} + 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(); } /** From ae38bf8d2e7cb125c3fb4a0944b93c4e121787eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 1 Jun 2020 20:01:05 -0400 Subject: [PATCH 68/90] remove `Applied` callback state --- dash-renderer/src/reducers/callbacks.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 5007c93add..f76a46f2b4 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -12,14 +12,12 @@ import { } from '../types/callbacks'; export enum CallbackActionType { - AddApplied = 'Callbacks.AddApplied', AddExecuted = 'Callbacks.AddExecuted', AddExecuting = 'Callbacks.AddExecuting', AddPrioritized = 'Callbacks.AddPrioritized', AddRequested = 'Callbacks.AddRequested', AddStored = 'Callbacks.AddStored', AddWatched = 'Callbacks.AddWatched', - RemoveApplied = 'Callbacks.RemoveApplied', RemoveExecuted = 'Callbacks.RemoveExecuted', RemoveExecuting = 'Callbacks.RemoveExecuting', RemovePrioritized = 'Callbacks.ReomvePrioritized', @@ -76,14 +74,12 @@ const DEFAULT_STATE: ICallbacksState = { const transforms: { [key: string]: (a1: ICallback[], a2: ICallback[]) => ICallback[] } = { - [CallbackActionType.AddApplied]: concat, [CallbackActionType.AddExecuted]: concat, [CallbackActionType.AddExecuting]: concat, [CallbackActionType.AddPrioritized]: concat, [CallbackActionType.AddRequested]: concat, [CallbackActionType.AddStored]: concat, [CallbackActionType.AddWatched]: concat, - [CallbackActionType.RemoveApplied]: difference, [CallbackActionType.RemoveExecuted]: difference, [CallbackActionType.RemoveExecuting]: difference, [CallbackActionType.RemovePrioritized]: difference, From 32aac969ee135e7dd05e3fc070c97954caf3b8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 1 Jun 2020 20:22:52 -0400 Subject: [PATCH 69/90] create priority hash that is sortable instead of carrying around the priority array --- dash-renderer/src/actions/dependencies_ts.ts | 13 +++++++------ .../src/observers/prioritizedCallbacks.ts | 14 +------------- dash-renderer/src/types/callbacks.ts | 2 +- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 1eabee9700..1f197ba88a 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -81,12 +81,11 @@ export function getCallbacksByInput( } /* - * 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. + * 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) { +export function getPriority(graphs: any, paths: any, callback: ICallback): string { let callbacks: ICallback[] = [callback]; let touchedOutputs: { [key: string]: boolean } = {}; let priority: number[] = []; @@ -123,7 +122,9 @@ export function getPriority(graphs: any, paths: any, callback: ICallback) { } } - return priority; + priority.unshift(priority.length); + + return map(i => Math.min(i, 35).toString(36), priority).join(''); } export const getReadyCallbacks = ( diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 1046d556f6..92a12e7b93 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -34,19 +34,7 @@ import { import { IStoreObserverDefinition } from '../StoreObserver'; const sortPriority = (c1: ICallback, c2: ICallback): number => { - const lDiff = c2.priority.length - c1.priority.length; - if (lDiff) { - return lDiff; - } - - for (let i = 0; i < c1.priority.length; ++i) { - const pDiff = c2.priority[i] - c1.priority[i]; - if (pDiff) { - return pDiff; - } - } - - return 0; + return c1.priority > c2.priority ? -1 : 1; } const observer: IStoreObserverDefinition = { diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index d47d88b459..4f976480cb 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -36,7 +36,7 @@ export interface ICallbackTemplate { } export interface ICallback extends ICallbackTemplate { - priority: number[]; + priority: string; } export interface IExecutingCallback extends ICallback { From dde23d3e3c8839c7346e41fcb73b124ea0e8ef3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 2 Jun 2020 09:00:38 -0400 Subject: [PATCH 70/90] noise From be6ced62f1bd9efdc32e82454366b929f21ffb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 2 Jun 2020 11:30:03 -0400 Subject: [PATCH 71/90] tslint --- dash-renderer/package-lock.json | 59 ++++ dash-renderer/package.json | 3 + dash-renderer/src/AppProvider.react.tsx | 8 +- dash-renderer/src/actions/callbacks.ts | 292 +++++++++--------- dash-renderer/src/actions/dependencies_ts.ts | 28 +- dash-renderer/src/actions/isLoading.ts | 4 +- dash-renderer/src/actions/loadingMap.ts | 4 +- .../src/observers/executedCallbacks.ts | 10 +- .../src/observers/executingCallbacks.ts | 6 +- dash-renderer/src/observers/isLoading.ts | 2 +- .../src/observers/prioritizedCallbacks.ts | 2 +- .../src/observers/requestedCallbacks.ts | 4 +- .../src/observers/storedCallbacks.ts | 4 +- dash-renderer/src/reducers/callbacks.ts | 2 +- dash-renderer/src/utils/TreeContainer.ts | 8 +- dash-renderer/tslint.json | 58 ++++ 16 files changed, 309 insertions(+), 185 deletions(-) create mode 100644 dash-renderer/tslint.json diff --git a/dash-renderer/package-lock.json b/dash-renderer/package-lock.json index cedeada412..c442b8c6ae 100644 --- a/dash-renderer/package-lock.json +++ b/dash-renderer/package-lock.json @@ -4703,6 +4703,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", @@ -5926,6 +5932,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", @@ -18559,12 +18571,59 @@ "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 5a8b777f42..cb76e295a3 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -7,8 +7,10 @@ "prepublishOnly": "rm -rf lib && babel src --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", @@ -70,6 +72,7 @@ "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", diff --git a/dash-renderer/src/AppProvider.react.tsx b/dash-renderer/src/AppProvider.react.tsx index 83cc7ba05e..6b534be900 100644 --- a/dash-renderer/src/AppProvider.react.tsx +++ b/dash-renderer/src/AppProvider.react.tsx @@ -18,15 +18,15 @@ const AppProvider = ({hooks}: any) => { 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/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 249a38f1c1..368061d493 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -7,11 +7,11 @@ import { path, pick, pluck, - zip, + zip } from 'ramda'; import { STATUS } from '../constants/constants'; -import { CallbackActionType, CallbackAggregateActionType } from "../reducers/callbacks"; +import { CallbackActionType, CallbackAggregateActionType } from '../reducers/callbacks'; import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, ICallbackPayload, IStoredCallback } from '../types/callbacks'; import { isMultiValued, stringifyId, isMultiOutputProp } from './dependencies'; import { urlBase } from './utils'; @@ -126,7 +126,7 @@ function fillVals( inputList.map(({ id, property, path: path_ }: any) => ({ id, property, - value: (path(path_, layout) as any).props[property], + value: (path(path_, layout) as any).props[property] })), specs[i], cb.anyVals, @@ -177,6 +177,150 @@ const getVals = (input: any) => 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: ICallback, config: any, @@ -248,153 +392,13 @@ export function executeCallback( } return null; } else { - handleServerside(payload) + handleServerside(hooks, config, payload) .then(data => resolve({ data, payload })) .catch(error => resolve({ error, payload })); } } catch (error) { resolve({ error, payload: null }); } - - 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; - } - - 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(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.'); - }); - } }); const newCb = { diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index 1f197ba88a..b9c45d4493 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -52,18 +52,18 @@ export function getCallbacksByInput( ); } else { // wildcard version - const keys = Object.keys(id).sort(); - const vals = props(keys, id); - const keyStr = keys.join(','); + 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)) { + if (idMatch(_keys, vals, pattern.values)) { pattern.callbacks.forEach( addAllResolvedFromOutputs( - resolveDeps(keys, vals, pattern.values), + resolveDeps(_keys, vals, pattern.values), paths, matches ) @@ -227,17 +227,17 @@ export const getUniqueIdentifier = ({ map(combineIdAndProp, [ ...inputs, ...outputs, - ...state, + ...state ]), Array.isArray(anyVals) ? anyVals : anyVals === '' ? [] : [anyVals] ).join(','); -export function includeObservers(id: any, props: any, graphs: any, paths: any): ICallback[] { +export function includeObservers(id: any, properties: any, graphs: any, paths: any): ICallback[] { return flatten(map( propName => getCallbacksByInput(graphs, paths, id, propName), - keys(props) + keys(properties) )); } @@ -260,7 +260,7 @@ export const makeResolvedCallback = ( getState: paths => callback.state.map(resolve(paths)), changedPropIds: {}, initialCall: false, - requestedOutputs: {}, + requestedOutputs: {} }); export function pruneCallbacks(callbacks: T[], paths: any): { @@ -297,9 +297,9 @@ export function resolveDeps(refKeys?: any, refVals?: any, refPatternVals?: strin 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 _keys = Object.keys(idPattern).sort(); + const patternVals = props(_keys, idPattern); + const keyStr = _keys.join(','); const keyPaths = paths.objs[keyStr]; if (!keyPaths) { return []; @@ -308,7 +308,7 @@ export function resolveDeps(refKeys?: any, refVals?: any, refPatternVals?: strin keyPaths.forEach(({ values: vals, path }: any) => { if ( idMatch( - keys, + _keys, vals, patternVals, refKeys, @@ -316,7 +316,7 @@ export function resolveDeps(refKeys?: any, refVals?: any, refPatternVals?: strin refPatternVals ) ) { - result.push({ id: zipObj(keys, vals), property, path }); + result.push({ id: zipObj(_keys, vals), property, path }); } }); return result; diff --git a/dash-renderer/src/actions/isLoading.ts b/dash-renderer/src/actions/isLoading.ts index 3842a2cfb8..a91feb4cac 100644 --- a/dash-renderer/src/actions/isLoading.ts +++ b/dash-renderer/src/actions/isLoading.ts @@ -1,5 +1,5 @@ -import { createAction } from "redux-actions"; +import { createAction } from 'redux-actions'; -import { IsLoadingActionType, IsLoadingState } from "../reducers/isLoading"; +import { IsLoadingActionType, IsLoadingState } from '../reducers/isLoading'; export const setIsLoading = createAction(IsLoadingActionType.Set); \ No newline at end of file diff --git a/dash-renderer/src/actions/loadingMap.ts b/dash-renderer/src/actions/loadingMap.ts index 1d76ad8fa7..c429e583f0 100644 --- a/dash-renderer/src/actions/loadingMap.ts +++ b/dash-renderer/src/actions/loadingMap.ts @@ -1,5 +1,5 @@ -import { createAction } from "redux-actions"; +import { createAction } from 'redux-actions'; -import { LoadingMapActionType, LoadingMapState } from "../reducers/loadingMap"; +import { LoadingMapActionType, LoadingMapState } from '../reducers/loadingMap'; export const setLoadingMap = createAction(LoadingMapActionType.Set); \ No newline at end of file diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index dd0bed7f46..f2ccaa759a 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -12,7 +12,7 @@ import { toPairs } from 'ramda'; -import { IStoreState } from "../store"; +import { IStoreState } from '../store'; import { aggregateCallbacks, @@ -80,7 +80,7 @@ const observer: IStoreObserverDefinition = { updateProps({ itempath, props, - source: 'response', + source: 'response' }) ); @@ -136,7 +136,7 @@ const observer: IStoreObserverDefinition = { requestedCallbacks = concat( requestedCallbacks, getLayoutCallbacks(graphs, paths, children, { - chunkPath: oldChildrenPath, + chunkPath: oldChildrenPath }) ); @@ -158,11 +158,11 @@ const observer: IStoreObserverDefinition = { appliedProps ); if (!isEmpty(addedProps)) { - const { graphs, paths } = getState(); + const { graphs: currentGraphs, paths } = getState(); requestedCallbacks = concat( requestedCallbacks, - includeObservers(id, addedProps, graphs, paths) + includeObservers(id, addedProps, currentGraphs, paths) ); } }, Object.entries(data)); diff --git a/dash-renderer/src/observers/executingCallbacks.ts b/dash-renderer/src/observers/executingCallbacks.ts index 76f93ac4ce..7517aeb12c 100644 --- a/dash-renderer/src/observers/executingCallbacks.ts +++ b/dash-renderer/src/observers/executingCallbacks.ts @@ -13,7 +13,7 @@ import { } from '../actions/callbacks'; import { - IExecutingCallback, + IExecutingCallback } from '../types/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; @@ -26,7 +26,7 @@ const observer: IStoreObserverDefinition = { const { callbacks: { executing - }, + } } = getState(); const [deferred, skippedOrReady] = partition(cb => cb.executionPromise instanceof Promise, executing); @@ -37,7 +37,7 @@ const observer: IStoreObserverDefinition = { skippedOrReady.length ? addExecutedCallbacks(skippedOrReady.map(cb => assoc('executionResult', cb.executionPromise as any, cb))) : null ])); - deferred.forEach(async function (cb: IExecutingCallback) { + deferred.forEach(async (cb: IExecutingCallback) => { const result = await cb.executionPromise; // Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled diff --git a/dash-renderer/src/observers/isLoading.ts b/dash-renderer/src/observers/isLoading.ts index 36f25e6221..30dd04f91c 100644 --- a/dash-renderer/src/observers/isLoading.ts +++ b/dash-renderer/src/observers/isLoading.ts @@ -1,6 +1,6 @@ import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; -import { getPendingCallbacks } from "../utils/callbacks"; +import { getPendingCallbacks } from '../utils/callbacks'; import { setIsLoading } from '../actions/isLoading'; diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 92a12e7b93..6b69c74af4 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -9,7 +9,7 @@ import { uniq } from 'ramda'; -import { IStoreState } from "../store"; +import { IStoreState } from '../store'; import { aggregateCallbacks, diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index b8dbda7a46..9810fb2b4d 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -12,7 +12,7 @@ import { values } from 'ramda'; -import { IStoreState } from "../store"; +import { IStoreState } from '../store'; import { aggregateCallbacks, @@ -41,7 +41,7 @@ import { IStoredCallback } from '../types/callbacks'; -import { getPendingCallbacks } from "../utils/callbacks"; +import { getPendingCallbacks } from '../utils/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; const observer: IStoreObserverDefinition = { diff --git a/dash-renderer/src/observers/storedCallbacks.ts b/dash-renderer/src/observers/storedCallbacks.ts index a02301139a..83b19518b9 100644 --- a/dash-renderer/src/observers/storedCallbacks.ts +++ b/dash-renderer/src/observers/storedCallbacks.ts @@ -50,9 +50,9 @@ const observer: IStoreObserverDefinition = { let dropped = reduce((res, [ executionGroup, - callbacks + executionGroupCallbacks ]) => !pendingGroups[executionGroup] ? - concat(res, callbacks) : + concat(res, executionGroupCallbacks) : res, [] as IStoredCallback[], toPairs(executionGroups) diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index f76a46f2b4..5654e98145 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -85,7 +85,7 @@ const transforms: { [CallbackActionType.RemovePrioritized]: difference, [CallbackActionType.RemoveRequested]: difference, [CallbackActionType.RemoveStored]: difference, - [CallbackActionType.RemoveWatched]: difference, + [CallbackActionType.RemoveWatched]: difference }; const fields: { diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index 67371fe459..7f5f2a7d14 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -1,7 +1,7 @@ -import { path, type, has } from "ramda"; +import { path, type, has } from 'ramda'; import Registry from '../registry'; -import { stringifyId } from "../actions/dependencies"; +import { stringifyId } from '../actions/dependencies'; function isLoadingComponent(layout: any) { validateComponent(layout); @@ -27,7 +27,7 @@ export function getLoadingState(componentLayout: any, componentPath: any, loadin return { is_loading: true, prop_name: idprop.property, - component_name: stringifyId(idprop.id), + component_name: stringifyId(idprop.id) }; } @@ -36,7 +36,7 @@ export function getLoadingState(componentLayout: any, componentPath: any, loadin return { is_loading: true, prop_name: idprops.property, - component_name: stringifyId(idprops.id), + component_name: stringifyId(idprops.id) }; } diff --git a/dash-renderer/tslint.json b/dash-renderer/tslint.json new file mode 100644 index 0000000000..5524bf3df1 --- /dev/null +++ b/dash-renderer/tslint.json @@ -0,0 +1,58 @@ +{ + "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, // to allow use of 'function' type + "eofline": false, + "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-open-brace", + "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": [] +} From 10185dd8688baed6cd24f1db9b5e925f79937546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 2 Jun 2020 13:55:30 -0400 Subject: [PATCH 72/90] eofline: true --- dash-renderer/src/StoreObserver.ts | 2 +- dash-renderer/src/actions/callbacks.ts | 2 +- dash-renderer/src/actions/dependencies_ts.ts | 2 +- dash-renderer/src/actions/isLoading.ts | 2 +- dash-renderer/src/actions/loadingMap.ts | 2 +- dash-renderer/src/observers/isLoading.ts | 2 +- dash-renderer/src/observers/loadingMap.ts | 2 +- dash-renderer/src/observers/prioritizedCallbacks.ts | 2 +- dash-renderer/src/observers/requestedCallbacks.ts | 2 +- dash-renderer/src/reducers/callbacks.ts | 2 +- dash-renderer/src/reducers/isLoading.ts | 2 +- dash-renderer/src/reducers/loadingMap.ts | 2 +- dash-renderer/src/types/callbacks.ts | 2 +- dash-renderer/src/utils/TreeContainer.ts | 2 +- dash-renderer/src/utils/callbacks.ts | 2 +- dash-renderer/tslint.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dash-renderer/src/StoreObserver.ts b/dash-renderer/src/StoreObserver.ts index 4c49dced11..4bc82382f0 100644 --- a/dash-renderer/src/StoreObserver.ts +++ b/dash-renderer/src/StoreObserver.ts @@ -110,4 +110,4 @@ export default class StoreObserver { this._observers ), 1 ); -} \ No newline at end of file +} diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index 368061d493..b69852c4a0 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -413,4 +413,4 @@ export function executeCallback( executionPromise: { error, payload: null } }; } -} \ No newline at end of file +} diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index b9c45d4493..b345a3597a 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -321,4 +321,4 @@ export function resolveDeps(refKeys?: any, refVals?: any, refPatternVals?: strin }); return result; }; -} \ No newline at end of file +} diff --git a/dash-renderer/src/actions/isLoading.ts b/dash-renderer/src/actions/isLoading.ts index a91feb4cac..a501211317 100644 --- a/dash-renderer/src/actions/isLoading.ts +++ b/dash-renderer/src/actions/isLoading.ts @@ -2,4 +2,4 @@ import { createAction } from 'redux-actions'; import { IsLoadingActionType, IsLoadingState } from '../reducers/isLoading'; -export const setIsLoading = createAction(IsLoadingActionType.Set); \ No newline at end of file +export const setIsLoading = createAction(IsLoadingActionType.Set); diff --git a/dash-renderer/src/actions/loadingMap.ts b/dash-renderer/src/actions/loadingMap.ts index c429e583f0..2e4834dbd8 100644 --- a/dash-renderer/src/actions/loadingMap.ts +++ b/dash-renderer/src/actions/loadingMap.ts @@ -2,4 +2,4 @@ import { createAction } from 'redux-actions'; import { LoadingMapActionType, LoadingMapState } from '../reducers/loadingMap'; -export const setLoadingMap = createAction(LoadingMapActionType.Set); \ No newline at end of file +export const setLoadingMap = createAction(LoadingMapActionType.Set); diff --git a/dash-renderer/src/observers/isLoading.ts b/dash-renderer/src/observers/isLoading.ts index 30dd04f91c..fc625d45a9 100644 --- a/dash-renderer/src/observers/isLoading.ts +++ b/dash-renderer/src/observers/isLoading.ts @@ -25,4 +25,4 @@ const observer: IStoreObserverDefinition = { inputs: ['callbacks'] }; -export default observer; \ No newline at end of file +export default observer; diff --git a/dash-renderer/src/observers/loadingMap.ts b/dash-renderer/src/observers/loadingMap.ts index 12a9277e80..8242143083 100644 --- a/dash-renderer/src/observers/loadingMap.ts +++ b/dash-renderer/src/observers/loadingMap.ts @@ -80,4 +80,4 @@ const observer: IStoreObserverDefinition = { inputs: ['callbacks.executing', 'callbacks.watched', 'callbacks.executed'] }; -export default observer; \ No newline at end of file +export default observer; diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 6b69c74af4..60a979a290 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -109,4 +109,4 @@ const observer: IStoreObserverDefinition = { inputs: ['callbacks.prioritized', 'callbacks.completed'] }; -export default observer; \ No newline at end of file +export default observer; diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 9810fb2b4d..20823fb4b2 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -260,4 +260,4 @@ const observer: IStoreObserverDefinition = { inputs: ['callbacks.requested', 'callbacks.completed'] }; -export default observer; \ No newline at end of file +export default observer; diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 5654e98145..9ab839534f 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -140,4 +140,4 @@ export default ( }, state, action.type === CallbackAggregateActionType.Aggregate ? action.payload : [action] -); \ No newline at end of file +); diff --git a/dash-renderer/src/reducers/isLoading.ts b/dash-renderer/src/reducers/isLoading.ts index 2ada3915e5..0a25260551 100644 --- a/dash-renderer/src/reducers/isLoading.ts +++ b/dash-renderer/src/reducers/isLoading.ts @@ -19,4 +19,4 @@ export default ( action: ILoadingMapAction ) => action.type === IsLoadingActionType.Set ? action.payload : - state; \ No newline at end of file + state; diff --git a/dash-renderer/src/reducers/loadingMap.ts b/dash-renderer/src/reducers/loadingMap.ts index 7c22d490bd..1fb31a208d 100644 --- a/dash-renderer/src/reducers/loadingMap.ts +++ b/dash-renderer/src/reducers/loadingMap.ts @@ -19,4 +19,4 @@ export default ( action: ILoadingMapAction ) => action.type === LoadingMapActionType.Set ? action.payload : - state; \ No newline at end of file + state; diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 4f976480cb..3da540ba51 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -66,4 +66,4 @@ export type CallbackResult = { data?: any; error?: Error; payload: ICallbackPayload | null; -} \ No newline at end of file +} diff --git a/dash-renderer/src/utils/TreeContainer.ts b/dash-renderer/src/utils/TreeContainer.ts index 7f5f2a7d14..cf9df689c1 100644 --- a/dash-renderer/src/utils/TreeContainer.ts +++ b/dash-renderer/src/utils/TreeContainer.ts @@ -75,4 +75,4 @@ export function validateComponent(componentDefinition: any) { JSON.stringify(componentDefinition, null, 2) ); } -} \ No newline at end of file +} diff --git a/dash-renderer/src/utils/callbacks.ts b/dash-renderer/src/utils/callbacks.ts index 4d8ffbfe0a..1d12921d22 100644 --- a/dash-renderer/src/utils/callbacks.ts +++ b/dash-renderer/src/utils/callbacks.ts @@ -12,4 +12,4 @@ export const getPendingCallbacks = ({ ...executing, ...watched, ...executed -]; \ No newline at end of file +]; diff --git a/dash-renderer/tslint.json b/dash-renderer/tslint.json index 5524bf3df1..639cb4dd6d 100644 --- a/dash-renderer/tslint.json +++ b/dash-renderer/tslint.json @@ -18,7 +18,7 @@ "array-type": false, "arrow-parens": [true, "ban-single-arg-parens"], "ban-types": false, // to allow use of 'function' type - "eofline": false, + "eofline": true, "max-classes-per-file": false, "max-line-length": false, "member-access": false, From 484a4e5d8a58d2cd661be80e1158b2e1698fccb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 2 Jun 2020 14:44:30 -0400 Subject: [PATCH 73/90] rename getLayoutCallbacks, getCallbacksInLayout --- dash-renderer/src/actions/dependencies.js | 2 +- dash-renderer/src/actions/dependencies_ts.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 43ef956645..2eacaeb837 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -1093,7 +1093,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 = []; diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index b345a3597a..df68c6d56c 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -17,7 +17,7 @@ import { zipObj } from 'ramda'; import { ICallback, ICallbackProperty, ICallbackDefinition, ILayoutCallbackProperty, ICallbackTemplate } from '../types/callbacks'; -import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getCallbacksInLayout, isMultiValued, idMatch } from './dependencies'; +import { addAllResolvedFromOutputs, splitIdAndProp, stringifyId, getUnfilteredLayoutCallbacks, isMultiValued, idMatch } from './dependencies'; import { getPath } from './paths'; export const DIRECT = 2; @@ -158,7 +158,7 @@ export const getLayoutCallbacks = ( options: any ): ICallback[] => { let exclusions: string[] = []; - let callbacks = getCallbacksInLayout( + let callbacks = getUnfilteredLayoutCallbacks( graphs, paths, layout, From c93389bf59f77aac3bcddeb8f4af13ccc34057e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 4 Jun 2020 12:21:47 -0400 Subject: [PATCH 74/90] circular callbacks entrypoint, callback predecessors and dropping loops --- dash-renderer/src/actions/dependencies_ts.ts | 5 + .../src/observers/executedCallbacks.ts | 25 ++++- .../src/observers/prioritizedCallbacks.ts | 2 +- .../src/observers/requestedCallbacks.ts | 102 +++++++++++++++--- dash-renderer/src/types/callbacks.ts | 3 +- .../integration/renderer/test_multi_output.py | 2 +- 6 files changed, 115 insertions(+), 24 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index df68c6d56c..f1855d1ecb 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -131,6 +131,11 @@ export const getReadyCallbacks = ( 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, diff --git a/dash-renderer/src/observers/executedCallbacks.ts b/dash-renderer/src/observers/executedCallbacks.ts index f2ccaa759a..90e3787dd7 100644 --- a/dash-renderer/src/observers/executedCallbacks.ts +++ b/dash-renderer/src/observers/executedCallbacks.ts @@ -91,6 +91,11 @@ const observer: IStoreObserverDefinition = { let storedCallbacks: IStoredCallback[] = []; forEach(cb => { + const predecessors = concat( + cb.predecessors ?? [], + [cb.callback] + ); + const { callback: { clientside_function, @@ -119,7 +124,10 @@ const observer: IStoreObserverDefinition = { flatten(map( prop => getCallbacksByInput(graphs, oldPaths, parsedId, prop, true), keys(props) - )) + )).map(rcb => ({ + ...rcb, + predecessors + })) ); // New layout - trigger callbacks for that explicitly @@ -137,7 +145,10 @@ const observer: IStoreObserverDefinition = { requestedCallbacks, getLayoutCallbacks(graphs, paths, children, { chunkPath: oldChildrenPath - }) + }).map(rcb => ({ + ...rcb, + predecessors + })) ); // Wildcard callbacks with array inputs (ALL / ALLSMALLER) need to trigger @@ -146,7 +157,10 @@ const observer: IStoreObserverDefinition = { requestedCallbacks, getLayoutCallbacks(graphs, oldPaths, oldChildren, { removedArrayInputsOnly: true, newPaths: paths, chunkPath: oldChildrenPath - }) + }).map(rcb => ({ + ...rcb, + predecessors + })) ); } @@ -162,7 +176,10 @@ const observer: IStoreObserverDefinition = { requestedCallbacks = concat( requestedCallbacks, - includeObservers(id, addedProps, currentGraphs, paths) + includeObservers(id, addedProps, currentGraphs, paths).map(rcb => ({ + ...rcb, + predecessors + })) ); } }, Object.entries(data)); diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 60a979a290..2ebb4b6f1a 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -34,7 +34,7 @@ import { import { IStoreObserverDefinition } from '../StoreObserver'; const sortPriority = (c1: ICallback, c2: ICallback): number => { - return c1.priority > c2.priority ? -1 : 1; + return (c1.priority ?? '') > (c2.priority ?? '') ? -1 : 1; } const observer: IStoreObserverDefinition = { diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 20823fb4b2..a201e7bdb7 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -5,6 +5,7 @@ import { filter, flatten, groupBy, + includes, intersection, isEmpty, isNil, @@ -49,11 +50,27 @@ const observer: IStoreObserverDefinition = { dispatch, getState }) => { - const { callbacks, callbacks: { prioritized, executing, watched, executed, stored }, paths } = getState(); + const { callbacks, callbacks: { prioritized, 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 */ @@ -144,23 +161,71 @@ const observer: IStoreObserverDefinition = { */ let readyCallbacks = getReadyCallbacks(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(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]) + }))); + } + } + /* - If: - - there are `requested` callbacks - - no `requested` callback can be promoted to `prioritized` - - no callbacks are `prioritized`, `executing`, `watched` or `executed` - Then: - - the `requested` callbacks form a ciruclar dependency and can never be executed - - prune them out of `requested` + TODO? + Clean up the `requested` list - during the dispatch phase, + it will be updated for real */ - const rCircular = ( - !readyCallbacks.length && - !prioritized.length && - !executing.length && - !watched.length && - !executed.length && - requested.length - ) ? requested : []; + requested = concat( + difference( + requested, + oldBlocked + ), + newBlocked + ); /* 5. Prune callbacks that became irrelevant in their `executionGroup` @@ -249,7 +314,10 @@ const observer: IStoreObserverDefinition = { wRemoved.length ? removeWatchedCallbacks(wRemoved) : null, wAdded.length ? addWatchedCallbacks(wAdded) : null, // Prune circular callbacks - rCircular.length ? removeRequestedCallbacks(rCircular) : null, + 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 diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 3da540ba51..147493f6d2 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -36,7 +36,8 @@ export interface ICallbackTemplate { } export interface ICallback extends ICallbackTemplate { - priority: string; + predecessors?: ICallbackDefinition[]; + priority?: string; } export interface IExecutingCallback extends ICallback { diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 8e3bbe11c1..202bc29e5a 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -134,7 +134,7 @@ def set_bc(a): ) # The new system does NOT trigger callbacks in circular dependencies - dash_duo.wait_for_text_to_equal("#c", "") + dash_duo.wait_for_text_to_equal("#c", "X") err_text = dash_duo.find_element("span.dash-fe-error__title").text assert err_text == "Circular Dependencies" From 4396e7e8dc56db580cdb0927677d7a7c971d8b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 5 Jun 2020 10:10:51 -0400 Subject: [PATCH 75/90] use resolved inputs/outputs when calculating callback readiness --- dash-renderer/src/actions/dependencies_ts.ts | 9 +++++++-- dash-renderer/src/observers/requestedCallbacks.ts | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index f1855d1ecb..be6b1ad84b 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -128,6 +128,7 @@ export function getPriority(graphs: any, paths: any, callback: ICallback): strin } export const getReadyCallbacks = ( + paths: any, candidates: ICallback[], callbacks: ICallback[] = candidates ): ICallback[] => { @@ -139,7 +140,11 @@ export const getReadyCallbacks = ( // Find all outputs of all active callbacks const outputs = map( combineIdAndProp, - reduce((o, cb) => concat(o, cb.callback.outputs), [], callbacks) + reduce( + (o, cb) => concat(o, flatten(cb.getOutputs(paths))), + [], + callbacks + ) ); // Make `outputs` hash table for faster access @@ -150,7 +155,7 @@ export const getReadyCallbacks = ( return filter( cb => all( cbp => !outputsMap[combineIdAndProp(cbp)], - cb.callback.inputs + flatten(cb.getInputs(paths)) ), candidates ); diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index a201e7bdb7..33d0481b73 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -159,7 +159,7 @@ const observer: IStoreObserverDefinition = { /* 4. Find `requested` callbacks that do not depend on a outstanding output (as either input or state) */ - let readyCallbacks = getReadyCallbacks(requested, pendingCallbacks); + let readyCallbacks = getReadyCallbacks(paths, requested, pendingCallbacks); let oldBlocked: ICallback[] = []; let newBlocked: ICallback[] = []; @@ -196,7 +196,7 @@ const observer: IStoreObserverDefinition = { candidates = candidates.slice(1); // Remaining candidates are not blocked by current assumptions - candidates = getReadyCallbacks(candidates, readyCallbacks); + candidates = getReadyCallbacks(paths, candidates, readyCallbacks); // Blocked requests need to make sure they have the callback as a predecessor const blockedByAssumptions = difference(candidates, candidates); From 6fa637c0ddf1879227a82f1cccc01bd943251d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 8 Jun 2020 18:01:29 -0400 Subject: [PATCH 76/90] optimize transition between `prioritized` and `executing` --- .../src/observers/prioritizedCallbacks.ts | 94 ++++++++++++------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 2ebb4b6f1a..648d71387a 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -1,12 +1,11 @@ import { flatten, includes, - map, partition, pluck, - reduce, sort, - uniq + uniq, + forEach } from 'ramda'; import { IStoreState } from '../store'; @@ -28,8 +27,7 @@ import isAppReady from '../actions/isAppReady'; import { ICallback, - IExecutingCallback, - ICallbackProperty + IExecutingCallback } from '../types/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; @@ -53,12 +51,18 @@ const observer: IStoreObserverDefinition = { // Order prioritized callbacks based on depth and breadth of callback chain prioritized = sort(sortPriority, prioritized); - prioritized = prioritized.slice(0, available); - if (!prioritized.length) { - return; - } - - const callbacks: [ICallback, any][] = prioritized.map(cb => { + // Divide between ready and waiting + const [ready, waiting] = partition(cb => isAppReady( + layout, + paths, + uniq(pluck('id', [ + ...flatten(cb.getInputs(paths)), + ...flatten(cb.getState(paths)) + ])) + ) === true, prioritized); + + // Execute sync callbacks + const readyCallbacks: [ICallback, any][] = ready.map(cb => { const { getOutputs } = cb; const allOutputs = getOutputs(paths); const flatOutputs: any[] = flatten(allOutputs); @@ -76,35 +80,57 @@ const observer: IStoreObserverDefinition = { return [cb, { allOutputs, allPropIds }]; }); - const ids = reduce((res, [cb]) => ([ - ...res, - ...flatten(cb.getInputs(paths)), - ...flatten(cb.getState(paths)) - ]), [] as ICallbackProperty[], callbacks); - - /* - Make sure the app is ready to execute callbacks impacting `ids` - */ - await isAppReady(layout, paths, uniq(pluck('id', ids))); - - /* - Make sure to only execute callbacks that are still in the `prioritized` list (isAppReady is async - state could have changed) - */ - const { callbacks: { prioritized: updatedPrioritized } } = getState(); - const [remainingCallbacks] = partition( - ([cb]) => includes(cb, updatedPrioritized), - callbacks - ); - - const executingCallbacks: IExecutingCallback[] = remainingCallbacks.map(([cb, stash]) => { + const syncExecutingCallbacks: IExecutingCallback[] = readyCallbacks.map(([cb, stash]) => { return executeCallback(cb, config, hooks, paths, layout, stash); }); dispatch(aggregateCallbacks([ - remainingCallbacks.length ? removePrioritizedCallbacks(map(([cb]) => cb, remainingCallbacks)) : null, - executingCallbacks.length ? addExecutingCallbacks(executingCallbacks) : null + ready.length ? removePrioritizedCallbacks(ready) : null, + syncExecutingCallbacks.length ? addExecutingCallbacks(syncExecutingCallbacks) : null ])); + // Execute async callbacks + const asyncAvailable = available - syncExecutingCallbacks.length; + + prioritized = prioritized.slice(0, asyncAvailable); + if (!prioritized.length) { + return; + } + + forEach(async cb => { + 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 })); + }); + cb.requestedOutputs = reqOut; + + // Make sure the app is ready to execute callbacks impacting `ids` + await isAppReady(layout, paths, uniq(pluck('id', [ + ...flatten(cb.getInputs(paths)), + ...flatten(cb.getState(paths)) + ]))); + + // Make + const { callbacks: { prioritized: updatedPrioritized } } = getState(); + if (!includes(cb, updatedPrioritized)) { + return; + } + + const executingCallback = executeCallback(cb, config, hooks, paths, layout, { allOutputs, allPropIds }); + + dispatch(aggregateCallbacks([ + removePrioritizedCallbacks([cb]), + addExecutingCallbacks([executingCallback]) + ])); + }, waiting); }, inputs: ['callbacks.prioritized', 'callbacks.completed'] }; From 0f87b63bff843c637d1aa29e6d6a44a49f6424cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 8 Jun 2020 19:18:17 -0400 Subject: [PATCH 77/90] remove `forceOnEqual` --- dash-renderer/src/TreeContainer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dash-renderer/src/TreeContainer.js b/dash-renderer/src/TreeContainer.js index 78c50b178a..bf317ad17d 100644 --- a/dash-renderer/src/TreeContainer.js +++ b/dash-renderer/src/TreeContainer.js @@ -112,7 +112,7 @@ class BaseTreeContainer extends Component { ); } - setProps(newProps, forceOnEqual = false) { + setProps(newProps) { const { _dashprivate_graphs, _dashprivate_dispatch, @@ -123,7 +123,7 @@ class BaseTreeContainer extends Component { const oldProps = this.getLayoutProps(); const {id} = oldProps; const changedProps = pickBy( - (val, key) => forceOnEqual || !equals(val, oldProps[key]), + (val, key) => !equals(val, oldProps[key]), newProps ); if (!isEmpty(changedProps)) { From 00862c36ef65a0ffa7e12099e78dd6aeefbcdabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 8 Jun 2020 20:51:22 -0400 Subject: [PATCH 78/90] prioritized callbacks --- .../src/observers/prioritizedCallbacks.ts | 48 +++++++++++++------ dash-renderer/src/reducers/callbacks.ts | 5 +- dash-renderer/src/types/callbacks.ts | 6 +++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 648d71387a..35520921ae 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -1,6 +1,7 @@ import { flatten, includes, + map, partition, pluck, sort, @@ -14,7 +15,8 @@ import { aggregateCallbacks, removePrioritizedCallbacks, addExecutingCallbacks, - executeCallback + executeCallback, + addPrioritizedCallbacks } from '../actions/callbacks'; import { stringifyId } from '../actions/dependencies'; @@ -45,14 +47,17 @@ const observer: IStoreObserverDefinition = { const available = Math.max( 0, - 10 - executing.length - watched.length + 12 - executing.length - watched.length - prioritized.filter(cb => cb.isReady).length ); + // Remove prioritized callbacks that are already waiting to move to `executing` + prioritized = prioritized.filter(cb => !cb.isReady); + // Order prioritized callbacks based on depth and breadth of callback chain prioritized = sort(sortPriority, prioritized); // Divide between ready and waiting - const [ready, waiting] = partition(cb => isAppReady( + let [ready, waiting] = partition(cb => isAppReady( layout, paths, uniq(pluck('id', [ @@ -61,8 +66,10 @@ const observer: IStoreObserverDefinition = { ])) ) === true, prioritized); + ready = ready.slice(0, available); + // Execute sync callbacks - const readyCallbacks: [ICallback, any][] = ready.map(cb => { + const readyCallbacks: [ICallback, any][] = map(cb => { const { getOutputs } = cb; const allOutputs = getOutputs(paths); const flatOutputs: any[] = flatten(allOutputs); @@ -78,7 +85,7 @@ const observer: IStoreObserverDefinition = { cb.requestedOutputs = reqOut; return [cb, { allOutputs, allPropIds }]; - }); + }, ready); const syncExecutingCallbacks: IExecutingCallback[] = readyCallbacks.map(([cb, stash]) => { return executeCallback(cb, config, hooks, paths, layout, stash); @@ -92,12 +99,14 @@ const observer: IStoreObserverDefinition = { // Execute async callbacks const asyncAvailable = available - syncExecutingCallbacks.length; - prioritized = prioritized.slice(0, asyncAvailable); - if (!prioritized.length) { + waiting = waiting.slice(0, asyncAvailable); + if (!waiting.length) { return; } - forEach(async cb => { + dispatch(removePrioritizedCallbacks(waiting)); + + waiting = map(cb => { const { getOutputs } = cb; const allOutputs = getOutputs(paths); const flatOutputs: any[] = flatten(allOutputs); @@ -110,13 +119,24 @@ const observer: IStoreObserverDefinition = { idOut.push(property); allPropIds.push(combineIdAndProp({ id: idStr, property })); }); - cb.requestedOutputs = reqOut; + return { + ...cb, + allOutputs, + allPropIds, + isReady: isAppReady(layout, paths, uniq(pluck('id', [ + ...flatten(cb.getInputs(paths)), + ...flatten(cb.getState(paths)) + ]))), + requestedOutputs: reqOut + }; + }, waiting); + + dispatch(addPrioritizedCallbacks(waiting)); + + forEach(async cb => { // Make sure the app is ready to execute callbacks impacting `ids` - await isAppReady(layout, paths, uniq(pluck('id', [ - ...flatten(cb.getInputs(paths)), - ...flatten(cb.getState(paths)) - ]))); + await cb.isReady; // Make const { callbacks: { prioritized: updatedPrioritized } } = getState(); @@ -124,7 +144,7 @@ const observer: IStoreObserverDefinition = { return; } - const executingCallback = executeCallback(cb, config, hooks, paths, layout, { allOutputs, allPropIds }); + const executingCallback = executeCallback(cb, config, hooks, paths, layout, cb); dispatch(aggregateCallbacks([ removePrioritizedCallbacks([cb]), diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 9ab839534f..53f0236d8e 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -8,7 +8,8 @@ import { ICallback, IExecutedCallback, IExecutingCallback, - IStoredCallback + IStoredCallback, + IPrioritizedCallback } from '../types/callbacks'; export enum CallbackActionType { @@ -53,7 +54,7 @@ type CallbackAction = export interface ICallbacksState { requested: ICallback[]; - prioritized: ICallback[]; + prioritized: IPrioritizedCallback[]; executing: IExecutingCallback[]; watched: IExecutingCallback[]; executed: IExecutedCallback[]; diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 147493f6d2..38693b444c 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -40,6 +40,12 @@ export interface ICallback extends ICallbackTemplate { priority?: string; } +export interface IPrioritizedCallback extends ICallback { + allOutputs: ILayoutCallbackProperty[][]; + allPropIds: any[]; + isReady?: Promise | true; +} + export interface IExecutingCallback extends ICallback { executionPromise: Promise | CallbackResult | null; } From 783372f804988ac403ad473c8853eb5978ab40b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 9 Jun 2020 09:34:14 -0400 Subject: [PATCH 79/90] handle async callbacks through an additional `blocked` state --- dash-renderer/src/actions/callbacks.ts | 10 +- dash-renderer/src/actions/dependencies.js | 2 - dash-renderer/src/actions/dependencies_ts.ts | 3 +- .../src/observers/executingCallbacks.ts | 20 +- .../src/observers/prioritizedCallbacks.ts | 172 ++++++++---------- .../src/observers/requestedCallbacks.ts | 23 ++- dash-renderer/src/reducers/callbacks.ts | 14 +- dash-renderer/src/types/callbacks.ts | 17 +- dash-renderer/src/utils/callbacks.ts | 2 + 9 files changed, 144 insertions(+), 119 deletions(-) diff --git a/dash-renderer/src/actions/callbacks.ts b/dash-renderer/src/actions/callbacks.ts index b69852c4a0..c660bcb218 100644 --- a/dash-renderer/src/actions/callbacks.ts +++ b/dash-renderer/src/actions/callbacks.ts @@ -12,12 +12,15 @@ import { import { STATUS } from '../constants/constants'; import { CallbackActionType, CallbackAggregateActionType } from '../reducers/callbacks'; -import { CallbackResult, ICallback, IExecutedCallback, IExecutingCallback, ICallbackPayload, IStoredCallback } from '../types/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 ); @@ -40,6 +43,9 @@ export const addWatchedCallbacks = createAction(CallbackAc export const removeExecutedCallbacks = createAction( CallbackActionType.RemoveExecuted ); +export const removeBlockedCallbacks = createAction( + CallbackActionType.RemoveBlocked +); export const removeExecutingCallbacks = createAction( CallbackActionType.RemoveExecuting ); @@ -322,7 +328,7 @@ function inputsToDict(inputs_list: any) { } export function executeCallback( - cb: ICallback, + cb: IPrioritizedCallback, config: any, hooks: any, paths: any, diff --git a/dash-renderer/src/actions/dependencies.js b/dash-renderer/src/actions/dependencies.js index 2eacaeb837..1d77086d1d 100644 --- a/dash-renderer/src/actions/dependencies.js +++ b/dash-renderer/src/actions/dependencies.js @@ -936,8 +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. - * requestedOutputs: object of {[idStr]: [props]} listing all the props - * actually requested for update. * } */ function getCallbackByOutput(graphs, paths, id, prop) { diff --git a/dash-renderer/src/actions/dependencies_ts.ts b/dash-renderer/src/actions/dependencies_ts.ts index be6b1ad84b..840f03d89f 100644 --- a/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash-renderer/src/actions/dependencies_ts.ts @@ -269,8 +269,7 @@ export const makeResolvedCallback = ( getInputs: paths => callback.inputs.map(resolve(paths)), getState: paths => callback.state.map(resolve(paths)), changedPropIds: {}, - initialCall: false, - requestedOutputs: {} + initialCall: false }); export function pruneCallbacks(callbacks: T[], paths: any): { diff --git a/dash-renderer/src/observers/executingCallbacks.ts b/dash-renderer/src/observers/executingCallbacks.ts index 7517aeb12c..ffc92141e4 100644 --- a/dash-renderer/src/observers/executingCallbacks.ts +++ b/dash-renderer/src/observers/executingCallbacks.ts @@ -1,20 +1,18 @@ import { - partition, assoc, - find + find, + forEach, + partition } from 'ramda'; import { + addExecutedCallbacks, + addWatchedCallbacks, aggregateCallbacks, removeExecutingCallbacks, - removeWatchedCallbacks, - addWatchedCallbacks, - addExecutedCallbacks + removeWatchedCallbacks } from '../actions/callbacks'; -import { - IExecutingCallback -} from '../types/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; import { IStoreState } from '../store'; @@ -37,12 +35,12 @@ const observer: IStoreObserverDefinition = { skippedOrReady.length ? addExecutedCallbacks(skippedOrReady.map(cb => assoc('executionResult', cb.executionPromise as any, cb))) : null ])); - deferred.forEach(async (cb: IExecutingCallback) => { + forEach(async cb => { const result = await cb.executionPromise; - // Check if it's been removed from the `watched` list since - on callback completion, another callback may be cancelled 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) { @@ -57,7 +55,7 @@ const observer: IStoreObserverDefinition = { executionResult: result }]) ])); - }); + }, deferred); }, inputs: ['callbacks.executing'] }; diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 35520921ae..411f164ea9 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -1,22 +1,23 @@ import { + find, flatten, - includes, + forEach, map, partition, pluck, sort, - uniq, - forEach + uniq } from 'ramda'; import { IStoreState } from '../store'; import { - aggregateCallbacks, - removePrioritizedCallbacks, + addBlockedCallbacks, addExecutingCallbacks, + aggregateCallbacks, executeCallback, - addPrioritizedCallbacks + removeBlockedCallbacks, + removePrioritizedCallbacks } from '../actions/callbacks'; import { stringifyId } from '../actions/dependencies'; @@ -28,8 +29,10 @@ import { import isAppReady from '../actions/isAppReady'; import { + IBlockedCallback, ICallback, - IExecutingCallback + ILayoutCallbackProperty, + IPrioritizedCallback } from '../types/callbacks'; import { IStoreObserverDefinition } from '../StoreObserver'; @@ -37,6 +40,26 @@ 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 observer: IStoreObserverDefinition = { observer: async ({ dispatch, @@ -47,17 +70,14 @@ const observer: IStoreObserverDefinition = { const available = Math.max( 0, - 12 - executing.length - watched.length - prioritized.filter(cb => cb.isReady).length + 12 - executing.length - watched.length ); - // Remove prioritized callbacks that are already waiting to move to `executing` - prioritized = prioritized.filter(cb => !cb.isReady); - // Order prioritized callbacks based on depth and breadth of callback chain prioritized = sort(sortPriority, prioritized); - // Divide between ready and waiting - let [ready, waiting] = partition(cb => isAppReady( + // Divide between sync and async + const [syncCallbacks, asyncCallbacks] = partition(cb => isAppReady( layout, paths, uniq(pluck('id', [ @@ -66,91 +86,57 @@ const observer: IStoreObserverDefinition = { ])) ) === true, prioritized); - ready = ready.slice(0, available); - - // Execute sync callbacks - const readyCallbacks: [ICallback, any][] = map(cb => { - 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 })); - }); - cb.requestedOutputs = reqOut; - - return [cb, { allOutputs, allPropIds }]; - }, ready); - - const syncExecutingCallbacks: IExecutingCallback[] = readyCallbacks.map(([cb, stash]) => { - return executeCallback(cb, config, hooks, paths, layout, stash); - }); - - dispatch(aggregateCallbacks([ - ready.length ? removePrioritizedCallbacks(ready) : null, - syncExecutingCallbacks.length ? addExecutingCallbacks(syncExecutingCallbacks) : null - ])); - - // Execute async callbacks - const asyncAvailable = available - syncExecutingCallbacks.length; - - waiting = waiting.slice(0, asyncAvailable); - if (!waiting.length) { - return; + 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 + )) + ])); } - dispatch(removePrioritizedCallbacks(waiting)); - - waiting = map(cb => { - 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 { - ...cb, - allOutputs, - allPropIds, - isReady: isAppReady(layout, paths, uniq(pluck('id', [ - ...flatten(cb.getInputs(paths)), - ...flatten(cb.getState(paths)) - ]))), - requestedOutputs: reqOut - }; - }, waiting); - - dispatch(addPrioritizedCallbacks(waiting)); - - forEach(async cb => { - // Make sure the app is ready to execute callbacks impacting `ids` - await cb.isReady; - - // Make - const { callbacks: { prioritized: updatedPrioritized } } = getState(); - if (!includes(cb, updatedPrioritized)) { - return; - } - - const executingCallback = executeCallback(cb, config, hooks, paths, layout, cb); + if (pickedAsyncCallbacks.length) { + const deffered = map( + cb => ({ + ...cb, + ...getStash(cb, paths), + isReady: isAppReady(layout, paths, uniq(pluck('id', [ + ...flatten(cb.getInputs(paths)), + ...flatten(cb.getState(paths)) + ]))) + }), + pickedAsyncCallbacks + ); dispatch(aggregateCallbacks([ - removePrioritizedCallbacks([cb]), - addExecutingCallbacks([executingCallback]) + removePrioritizedCallbacks(pickedAsyncCallbacks), + addBlockedCallbacks(deffered) ])); - }, waiting); + + 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'] }; diff --git a/dash-renderer/src/observers/requestedCallbacks.ts b/dash-renderer/src/observers/requestedCallbacks.ts index 33d0481b73..1ca30a3ba1 100644 --- a/dash-renderer/src/observers/requestedCallbacks.ts +++ b/dash-renderer/src/observers/requestedCallbacks.ts @@ -24,7 +24,9 @@ import { addRequestedCallbacks, addPrioritizedCallbacks, addExecutingCallbacks, - addWatchedCallbacks + addWatchedCallbacks, + removeBlockedCallbacks, + addBlockedCallbacks } from '../actions/callbacks'; import { isMultiValued } from '../actions/dependencies'; @@ -39,7 +41,8 @@ import { import { ICallback, IExecutingCallback, - IStoredCallback + IStoredCallback, + IBlockedCallback } from '../types/callbacks'; import { getPendingCallbacks } from '../utils/callbacks'; @@ -50,7 +53,7 @@ const observer: IStoreObserverDefinition = { dispatch, getState }) => { - const { callbacks, callbacks: { prioritized, executing, watched, stored }, paths } = getState(); + const { callbacks, callbacks: { prioritized, blocked, executing, watched, stored }, paths } = getState(); let { callbacks: { requested } } = getState(); const pendingCallbacks = getPendingCallbacks(callbacks); @@ -114,6 +117,16 @@ const observer: IStoreObserverDefinition = { ) )); + 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( @@ -140,6 +153,7 @@ const observer: IStoreObserverDefinition = { 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); @@ -302,6 +316,7 @@ const observer: IStoreObserverDefinition = { // 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 @@ -309,6 +324,8 @@ const observer: IStoreObserverDefinition = { 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, diff --git a/dash-renderer/src/reducers/callbacks.ts b/dash-renderer/src/reducers/callbacks.ts index 53f0236d8e..81d24e65ab 100644 --- a/dash-renderer/src/reducers/callbacks.ts +++ b/dash-renderer/src/reducers/callbacks.ts @@ -9,16 +9,20 @@ import { IExecutedCallback, IExecutingCallback, IStoredCallback, - IPrioritizedCallback + 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', @@ -55,14 +59,16 @@ type CallbackAction = export interface ICallbacksState { requested: ICallback[]; prioritized: IPrioritizedCallback[]; + blocked: IBlockedCallback[]; executing: IExecutingCallback[]; - watched: IExecutingCallback[]; + watched: IWatchedCallback[]; executed: IExecutedCallback[]; stored: IStoredCallback[]; completed: number; } const DEFAULT_STATE: ICallbacksState = { + blocked: [], executed: [], executing: [], prioritized: [], @@ -75,12 +81,14 @@ const DEFAULT_STATE: ICallbacksState = { 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, @@ -92,12 +100,14 @@ const transforms: { 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', diff --git a/dash-renderer/src/types/callbacks.ts b/dash-renderer/src/types/callbacks.ts index 38693b444c..14882607d5 100644 --- a/dash-renderer/src/types/callbacks.ts +++ b/dash-renderer/src/types/callbacks.ts @@ -31,7 +31,6 @@ export interface ICallbackTemplate { getInputs: (paths: any) => ILayoutCallbackProperty[][]; getOutputs: (paths: any) => ILayoutCallbackProperty[][]; getState: (paths: any) => ILayoutCallbackProperty[][]; - requestedOutputs: { [key: string]: any }; resolvedId: any; } @@ -40,17 +39,27 @@ export interface ICallback extends ICallbackTemplate { 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; + isReady: Promise | true; } -export interface IExecutingCallback extends ICallback { +export interface IExecutingCallback extends IPrioritizedCallback { executionPromise: Promise | CallbackResult | null; } -export interface IExecutedCallback extends IExecutingCallback { +// tslint:disable-next-line:no-empty-interface +export interface IWatchedCallback extends IExecutingCallback { + +} + +export interface IExecutedCallback extends IWatchedCallback { executionResult: CallbackResult | null; } diff --git a/dash-renderer/src/utils/callbacks.ts b/dash-renderer/src/utils/callbacks.ts index 1d12921d22..b481577791 100644 --- a/dash-renderer/src/utils/callbacks.ts +++ b/dash-renderer/src/utils/callbacks.ts @@ -3,12 +3,14 @@ import { ICallbacksState } from '../reducers/callbacks'; export const getPendingCallbacks = ({ executed, executing, + blocked, prioritized, requested, watched }: ICallbacksState) => [ ...requested, ...prioritized, + ...blocked, ...executing, ...watched, ...executed From ecc5d8b6624f434fe87f4bba4e3e8ac42bdb5787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 9 Jun 2020 09:40:51 -0400 Subject: [PATCH 80/90] more "future proof" pendingCallbacks --- dash-renderer/src/utils/callbacks.ts | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/dash-renderer/src/utils/callbacks.ts b/dash-renderer/src/utils/callbacks.ts index b481577791..14befb6e7c 100644 --- a/dash-renderer/src/utils/callbacks.ts +++ b/dash-renderer/src/utils/callbacks.ts @@ -1,17 +1,8 @@ +import { omit, values } from 'ramda'; + import { ICallbacksState } from '../reducers/callbacks'; +import { ICallback } from '../types/callbacks'; -export const getPendingCallbacks = ({ - executed, - executing, - blocked, - prioritized, - requested, - watched -}: ICallbacksState) => [ - ...requested, - ...prioritized, - ...blocked, - ...executing, - ...watched, - ...executed -]; +export const getPendingCallbacks = (state: ICallbacksState) => Array().concat( + ...values(omit(['stored', 'completed'], state)) +); From 648e4a8d7b18248c461fc664e73d6be10b215ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 9 Jun 2020 19:07:36 -0400 Subject: [PATCH 81/90] babel typescript preset for babel transform --- dash-renderer/babel.config.js | 5 + dash-renderer/package-lock.json | 236 ++++++++++++++++++++++++++++++++ dash-renderer/package.json | 2 + 3 files changed, 243 insertions(+) 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/package-lock.json b/dash-renderer/package-lock.json index c442b8c6ae..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", diff --git a/dash-renderer/package.json b/dash-renderer/package.json index cb76e295a3..dd2962f1f6 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -43,9 +43,11 @@ "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", From 3c782c11063c31589058d0a318211f0b7d237c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 9 Jun 2020 19:19:33 -0400 Subject: [PATCH 82/90] ts(x?) and js(x?) extensions --- dash-renderer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash-renderer/package.json b/dash-renderer/package.json index dd2962f1f6..a2c1b48606 100644 --- a/dash-renderer/package.json +++ b/dash-renderer/package.json @@ -4,7 +4,7 @@ "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", From 441b6532b8b266e7371d91962d4a5092057e4db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 11 Jun 2020 14:24:37 -0400 Subject: [PATCH 83/90] dry --- .../src/observers/prioritizedCallbacks.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dash-renderer/src/observers/prioritizedCallbacks.ts b/dash-renderer/src/observers/prioritizedCallbacks.ts index 411f164ea9..0b6efeb2ba 100644 --- a/dash-renderer/src/observers/prioritizedCallbacks.ts +++ b/dash-renderer/src/observers/prioritizedCallbacks.ts @@ -60,6 +60,11 @@ const getStash = (cb: IPrioritizedCallback, paths: any): { 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, @@ -80,10 +85,7 @@ const observer: IStoreObserverDefinition = { const [syncCallbacks, asyncCallbacks] = partition(cb => isAppReady( layout, paths, - uniq(pluck('id', [ - ...flatten(cb.getInputs(paths)), - ...flatten(cb.getState(paths)) - ])) + getIds(cb, paths) ) === true, prioritized); const pickedSyncCallbacks = syncCallbacks.slice(0, available); @@ -104,10 +106,7 @@ const observer: IStoreObserverDefinition = { cb => ({ ...cb, ...getStash(cb, paths), - isReady: isAppReady(layout, paths, uniq(pluck('id', [ - ...flatten(cb.getInputs(paths)), - ...flatten(cb.getState(paths)) - ]))) + isReady: isAppReady(layout, paths, getIds(cb, paths)) }), pickedAsyncCallbacks ); From a5f9c3148c6c05342dff1054bed2c320c12403dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 12 Jun 2020 11:58:55 -0400 Subject: [PATCH 84/90] remove comments --- dash-renderer/tslint.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dash-renderer/tslint.json b/dash-renderer/tslint.json index 639cb4dd6d..600dce2dfc 100644 --- a/dash-renderer/tslint.json +++ b/dash-renderer/tslint.json @@ -17,7 +17,7 @@ "rules": { "array-type": false, "arrow-parens": [true, "ban-single-arg-parens"], - "ban-types": false, // to allow use of 'function' type + "ban-types": false, "eofline": true, "max-classes-per-file": false, "max-line-length": false, @@ -34,7 +34,6 @@ "check-catch", "check-finally", "check-else", - // "check-open-brace", "check-whitespace" ], "only-arrow-functions": [ From ca02d5578f13eb39acb111a33d1f4f1f84477282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 09:36:34 -0400 Subject: [PATCH 85/90] revert comment change --- tests/integration/renderer/test_multi_output.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/renderer/test_multi_output.py b/tests/integration/renderer/test_multi_output.py index 202bc29e5a..4741471420 100644 --- a/tests/integration/renderer/test_multi_output.py +++ b/tests/integration/renderer/test_multi_output.py @@ -133,7 +133,9 @@ def set_bc(a): dev_tools_hot_reload=False, ) - # The new system does NOT trigger callbacks in circular dependencies + # the UI still renders the output triggered by callback. + # The new system does NOT loop infinitely like it used to, each callback + # is invoked no more than once. dash_duo.wait_for_text_to_equal("#c", "X") err_text = dash_duo.find_element("span.dash-fe-error__title").text From b8af7e34a1bf3932b739ecac6567377e344c8b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 09:40:41 -0400 Subject: [PATCH 86/90] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a197a119f..8f50a542a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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 From afcdba90b68c178303ec68140ae7a07005d9c4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 15:43:33 -0400 Subject: [PATCH 87/90] - undo `redux_state_rqs` regression - new `redux_state_is_loading` method --- dash/testing/dash_page.py | 14 +++++++++++++- tests/integration/callbacks/test_basic_callback.py | 4 ++-- .../callbacks/test_layout_paths_with_callbacks.py | 2 +- tests/integration/callbacks/test_missing_inputs.py | 2 +- .../callbacks/test_multiple_callbacks.py | 2 +- tests/integration/renderer/test_dependencies.py | 2 +- tests/integration/renderer/test_due_diligence.py | 2 +- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 43523dcad5..7f69438821 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -36,7 +36,19 @@ def redux_state_paths(self): def redux_state_rqs(self): return self.driver.execute_script( """ - return !window.store.getState().isLoading; + 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; """ ) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 799bed9d95..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, "loadingMap 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_layout_paths_with_callbacks.py b/tests/integration/callbacks/test_layout_paths_with_callbacks.py index 1c3c4e5d30..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, "loadingMap 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 1810d83421..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 3c21c2a716..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/renderer/test_dependencies.py b/tests/integration/renderer/test_dependencies.py index b207689e1b..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 de51b05f77..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" From 4a083bbbeb8fb78923cfca2304b22144b6752fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 16:10:55 -0400 Subject: [PATCH 88/90] lint --- dash/dash.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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" From be36ab47ae5977e71a2a225175b353647e5f7531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 16:44:56 -0400 Subject: [PATCH 89/90] test number of callbacks triggered for wildcard `allsmaller` --- tests/integration/callbacks/test_wildcards.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) 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) From 63f14afffe1c91d6a00b8a688da2a62f804a46e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 15 Jun 2020 16:51:32 -0400 Subject: [PATCH 90/90] redux_state_rqs legacy support --- dash/testing/dash_page.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dash/testing/dash_page.py b/dash/testing/dash_page.py index 7f69438821..caedb9b5e7 100644 --- a/dash/testing/dash_page.py +++ b/dash/testing/dash_page.py @@ -36,6 +36,20 @@ def redux_state_paths(self): def redux_state_rqs(self): return self.driver.execute_script( """ + + // 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;