Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Commit 401b977

Browse files
authored
Merge pull request #22 from plotly/fix-request-order
match response order with request order
2 parents f10c91e + bf6a523 commit 401b977

13 files changed

+219
-93
lines changed

Diff for: CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [0.11.0] - 2017-09-28
6+
### Fixed
7+
- 🐞 Previously, old requests could override new requests if their response was longer than the new one.
8+
This caused subtle bugs when apps are deployed on multiple processes or threads with component
9+
callbacks that update at varying rates like urls. Originally reported in github.com/plotly/dash/issues/133. This fix should also improve performance when many updates happen at once as outdated requests will get dropped instead of updating the UI.
10+
511
## [0.10.0] - 2017-09-19
612
### Fixed
713
- Fixed an issue where a callback would be fired on page load and when dynamically generated excessively. Previously, the callback would be called as many times as it had inputs. Now, it is called less. https://github.com/plotly/dash-renderer/pull/21
814
### Maintenance
915
- Add percy screenshot tests
1016

1117

18+
1219
## [0.9.0] - 2017-09-07
1320
### Fixed
1421
- 🐞 Fixed a bug where Dash would fire updates for each parent of a grandchild node that shared the same grandparent. Originally reported in https://community.plot.ly/t/specifying-dependency-tree-traversal/5080/5

Diff for: dash_renderer/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.10.0'
1+
__version__ = '0.11.0'

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dash-renderer",
3-
"version": "0.10.0",
3+
"version": "0.11.0",
44
"description": "render dash components in react",
55
"main": "src/index.js",
66
"scripts": {

Diff for: src/APIController.react.js

+4-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {connect} from 'react-redux'
2-
import {any, contains, equals, isEmpty, isNil} from 'ramda'
2+
import {contains, isEmpty, isNil} from 'ramda'
33
import React, {Component, PropTypes} from 'react';
4-
import renderTree from './renderTree';
4+
import TreeContainer from './TreeContainer';
55
import {
66
computeGraphs,
77
computePaths,
@@ -10,7 +10,6 @@ import {
1010
} from './actions/index';
1111
import {getDependencies, getLayout} from './actions/api';
1212
import {APP_STATES} from './reducers/constants';
13-
import AccessDenied from './AccessDenied.react';
1413

1514
/**
1615
* Fire off API calls for initialization
@@ -75,24 +74,12 @@ class UnconnectedContainer extends Component {
7574
render () {
7675
const {
7776
appLifecycle,
78-
config,
7977
dependenciesRequest,
80-
lastUpdateComponentRequest,
8178
layoutRequest,
8279
layout
8380
} = this.props;
8481

85-
// Auth protected routes
86-
if (any(equals(true),
87-
[dependenciesRequest, lastUpdateComponentRequest,
88-
layoutRequest].map(
89-
request => (request.status && request.status === 403))
90-
)) {
91-
return (<AccessDenied config={config}/>);
92-
}
93-
94-
95-
else if (layoutRequest.status &&
82+
if (layoutRequest.status &&
9683
!contains(layoutRequest.status, [200, 'loading'])
9784
) {
9885
return (<div>{'Error loading layout'}</div>);
@@ -110,7 +97,7 @@ class UnconnectedContainer extends Component {
11097
else if (appLifecycle === APP_STATES('HYDRATED')) {
11198
return (
11299
<div id="_dash-app-content">
113-
{renderTree(layout, dependenciesRequest.content)}
100+
<TreeContainer layout={layout}/>
114101
</div>
115102
);
116103
}
@@ -126,9 +113,7 @@ UnconnectedContainer.propTypes = {
126113
APP_STATES('HYDRATED')
127114
]),
128115
dispatch: PropTypes.function,
129-
config: PropTypes.object,
130116
dependenciesRequest: PropTypes.object,
131-
lastUpdateComponentRequest: PropTypes.objec,
132117
layoutRequest: PropTypes.object,
133118
layout: PropTypes.object,
134119
paths: PropTypes.object,
@@ -139,9 +124,7 @@ const Container = connect(
139124
// map state to props
140125
state => ({
141126
appLifecycle: state.appLifecycle,
142-
config: state.config,
143127
dependenciesRequest: state.dependenciesRequest,
144-
lastUpdateComponentRequest: state.lastUpdateComponentRequest,
145128
layoutRequest: state.layoutRequest,
146129
layout: state.layout,
147130
graphs: state.graphs,

Diff for: src/renderTree.js renamed to src/TreeContainer.js

+22-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
'use strict'
22

33
import R from 'ramda';
4-
import React, {PropTypes} from 'react';
4+
import React, {Component, PropTypes} from 'react';
55
import Registry from './registry';
66
import NotifyObservers from './components/core/NotifyObservers.react';
77

8-
export default function render(component) {
8+
export default class TreeContainer extends Component {
9+
shouldComponentUpdate(nextProps) {
10+
return nextProps.layout !== this.props.layout;
11+
}
12+
13+
render() {
14+
return render(this.props.layout);
15+
}
16+
}
17+
18+
TreeContainer.propTypes = {
19+
layout: PropTypes.object,
20+
}
21+
22+
function render(component) {
923
if (R.contains(R.type(component), ['String', 'Number', 'Null'])) {
1024
return component;
1125
}
1226

1327
// Create list of child elements
1428
let children;
1529

16-
const props = R.propOr({}, 'props', component);
30+
const componentProps = R.propOr({}, 'props', component);
1731

1832
if (!R.has('props', component) ||
1933
!R.has('children', component.props) ||
@@ -34,8 +48,9 @@ export default function render(component) {
3448
// One or multiple objects
3549
// Recursively render the tree
3650
// TODO - I think we should pass in `key` here.
37-
children = (Array.isArray(props.children) ? props.children : [props.children])
38-
.map(render);
51+
children = (Array.isArray(componentProps.children) ?
52+
componentProps.children : [componentProps.children])
53+
.map(render);
3954

4055
}
4156

@@ -60,13 +75,12 @@ export default function render(component) {
6075
);
6176

6277
return (
63-
<NotifyObservers id={props.id}>
78+
<NotifyObservers id={componentProps.id}>
6479
{parent}
6580
</NotifyObservers>
6681
);
6782
}
6883

6984
render.propTypes = {
70-
children: PropTypes.object,
71-
id: PropTypes.string
85+
children: PropTypes.object
7286
}

Diff for: src/actions/index.js

+102-18
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
/* global fetch:true, Promise:true, document:true */
22
import {
3+
__,
4+
adjust,
5+
any,
36
concat,
47
contains,
8+
findIndex,
9+
findLastIndex,
510
has,
611
intersection,
712
isEmpty,
813
keys,
914
lensPath,
15+
merge,
1016
pluck,
11-
reject,
17+
propEq,
1218
slice,
1319
sort,
1420
type,
15-
union,
1621
view
1722
} from 'ramda';
1823
import {createAction} from 'redux-actions';
1924
import {crawlLayout, hasId} from '../reducers/utils';
2025
import {APP_STATES} from '../reducers/constants';
2126
import {ACTIONS} from './constants';
2227
import cookie from 'cookie';
23-
import {urlBase} from '../utils';
28+
import {uid, urlBase} from '../utils';
2429

2530
export const updateProps = createAction(ACTIONS('ON_PROP_CHANGE'));
2631
export const setRequestQueue = createAction(ACTIONS('SET_REQUEST_QUEUE'));
@@ -253,10 +258,24 @@ export function notifyObservers(payload) {
253258
* the change for Child. if this update has already been queued up,
254259
* then skip the update for the other component
255260
*/
256-
const controllersInExistingQueue = intersection(
257-
requestQueue, controllers
261+
const controllerIsInExistingQueue = any(r =>
262+
contains(r.controllerId, controllers) && r.status === 'loading',
263+
requestQueue
258264
);
259265

266+
/*
267+
* TODO - Place throttling logic here?
268+
*
269+
* Only process the last two requests for a _single_ output
270+
* at a time.
271+
*
272+
* For example, if A -> B, and A is changed 10 times, then:
273+
* 1 - processing the first two requests
274+
* 2 - if more than 2 requests come in while the first two
275+
* are being processed, then skip updating all of the
276+
* requests except for the last 2
277+
*/
278+
260279
/*
261280
* also check that this observer is actually in the current
262281
* component tree.
@@ -267,7 +286,7 @@ export function notifyObservers(payload) {
267286
if (
268287
(controllersInFutureQueue.length === 0) &&
269288
(has(outputComponentId, getState().paths)) &&
270-
(controllersInExistingQueue.length === 0)
289+
!controllerIsInExistingQueue
271290
) {
272291
queuedObservers.push(outputIdAndProp)
273292
}
@@ -278,7 +297,20 @@ export function notifyObservers(payload) {
278297
* updated in a queue. not all of these requests will be fired in this
279298
* action
280299
*/
281-
dispatch(setRequestQueue(union(queuedObservers, requestQueue)));
300+
const newRequestQueue = queuedObservers.map(
301+
i => ({
302+
controllerId: i,
303+
status: 'loading',
304+
uid: uid(),
305+
requestTime: Date.now()
306+
})
307+
)
308+
dispatch(setRequestQueue(
309+
concat(
310+
requestQueue,
311+
newRequestQueue
312+
)
313+
));
282314

283315
const promises = [];
284316
for (let i = 0; i < queuedObservers.length; i++) {
@@ -351,20 +383,71 @@ export function notifyObservers(payload) {
351383
credentials: 'same-origin',
352384
body: JSON.stringify(payload)
353385
}).then(function handleResponse(res) {
354-
dispatch({
355-
type: 'lastUpdateComponentRequest',
356-
payload: {status: res.status}
357-
});
358386

359-
// clear this item from the request queue
360-
dispatch(setRequestQueue(
361-
reject(
362-
id => id === outputIdAndProp,
387+
const getThisRequestIndex = () => {
388+
const postRequestQueue = getState().requestQueue;
389+
const requestUid = newRequestQueue[i].uid;
390+
const thisRequestIndex = findIndex(
391+
propEq('uid', requestUid),
392+
postRequestQueue
393+
);
394+
return thisRequestIndex;
395+
}
396+
397+
const updateRequestQueue = rejected => {
398+
const postRequestQueue = getState().requestQueue
399+
const thisRequestIndex = getThisRequestIndex();
400+
const updatedQueue = adjust(
401+
merge(__, {
402+
status: res.status,
403+
responseTime: Date.now(),
404+
rejected
405+
}),
406+
thisRequestIndex,
407+
postRequestQueue
408+
);
409+
410+
dispatch(setRequestQueue(updatedQueue));
411+
}
412+
413+
const isRejected = () => {
414+
const latestRequestIndex = findLastIndex(
415+
propEq('controllerId', newRequestQueue[i].controllerId),
363416
getState().requestQueue
364-
)
365-
));
417+
);
418+
const rejected = latestRequestIndex > getThisRequestIndex();
419+
return rejected;
420+
}
421+
422+
if (res.status !== 200) {
423+
// update the status of this request
424+
updateRequestQueue(true);
425+
return;
426+
}
427+
428+
/*
429+
* Check to see if another request has already come back
430+
* _after_ this one.
431+
* If so, ignore this request.
432+
*/
433+
if (isRejected()) {
434+
updateRequestQueue(true);
435+
return;
436+
}
366437

367438
return res.json().then(function handleJson(data) {
439+
/*
440+
* Even if the `res` was received in the correct order,
441+
* the remainder of the response (res.json()) could happen
442+
* at different rates causing the parsed responses to
443+
* get out of order
444+
*/
445+
if (isRejected()) {
446+
updateRequestQueue(true);
447+
return;
448+
}
449+
450+
updateRequestQueue(false);
368451

369452
/*
370453
* it's possible that this output item is no longer visible.
@@ -467,7 +550,8 @@ export function notifyObservers(payload) {
467550

468551
}
469552

470-
})}));
553+
});
554+
}));
471555

472556
}
473557

Diff for: src/components/core/DocumentTitle.react.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* global document:true */
22

33
import {connect} from 'react-redux'
4-
import {isEmpty} from 'ramda'
4+
import {any} from 'ramda'
55
import {Component, PropTypes} from 'react'
66

77
class DocumentTitle extends Component {
@@ -13,7 +13,7 @@ class DocumentTitle extends Component {
1313
}
1414

1515
componentWillReceiveProps(props) {
16-
if (!isEmpty(props.requestQueue)) {
16+
if (any(r => r.status === 'loading', props.requestQueue)) {
1717
document.title = 'Updating...';
1818
} else {
1919
document.title = this.state.initialTitle;

Diff for: src/components/core/Loading.react.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {connect} from 'react-redux'
2-
import {isEmpty} from 'ramda'
2+
import {any} from 'ramda'
33
import React, {PropTypes} from 'react'
44

55
function Loading(props) {
6-
if (!isEmpty(props.requestQueue)) {
6+
if (any(r => r.status === 'loading', props.requestQueue)) {
77
return (
88
<div className="_dash-loading-callback"/>
9-
)
9+
);
1010
} else {
1111
return null;
1212
}

Diff for: src/reducers/api.js

-3
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,5 @@ function createApiReducer(store) {
2727
}
2828

2929
export const dependenciesRequest = createApiReducer('dependenciesRequest');
30-
export const lastUpdateComponentRequest = createApiReducer(
31-
'lastUpdateComponentRequest'
32-
);
3330
export const layoutRequest = createApiReducer('layoutRequest');
3431
export const loginRequest = createApiReducer('loginRequest');

0 commit comments

Comments
 (0)