Skip to content

Commit

Permalink
Use a single worker for node labeling
Browse files Browse the repository at this point in the history
- Add a workerAgent to multiplex messages
- Make the user functions ignorant of Worker boilerplate
- Remove externalData for now; see #413
- Formally make Node a container; avoid sending label props
  through ancestors
  • Loading branch information
bryfox committed Jul 26, 2018
1 parent f89e524 commit dd2aebc
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 101 deletions.
34 changes: 0 additions & 34 deletions public/protocols/development.netcanvas/node-label-worker.js

This file was deleted.

61 changes: 61 additions & 0 deletions public/protocols/development.netcanvas/nodeLabelWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @function
* @name nodeLabelWorker
* @description Generate a display label for a node.
*
* You may create any label based on input data; see examples below.
* The variable registry is not directly accessible here; any properties should
* be in sync with the registry defined in protocol.json.
*
* Do not change the name of this function.
*
* @param {Object} data
* @param {Object} data.node All props for the node requiring a label
* @param {Object} data.network The current state of the network in this session
* @param {Array} data.network.nodes
* @param {Array} data.network.edges
*
* @return {string|Promise} a label for the input node, or
* a promise that resolves to the label
*/
function nodeLabelWorker({ node, network }) {
// Examples:
//
// 1. Given name, surname initial
// const label = `${node.name} ${(node.last_name && `${node.last_name[0]}.`) || ''}`;
//
// 2. Counter based on data set or subset (here, the network nodes)
// const index = network.nodes.findIndex(n => n.uid === node.uid);
// const label = index > -1 ? `${node.name} ${index + 1}` : node.name;
//
// 3. Counter, based on network as well. Access to `externalData` TBD.
// const networkNodeIds = {};
// network.nodes.forEach((n) => { networkNodeIds[n.uid] = 1; });
// const externalNodes = externalData.previousInterview.nodes.filter(n => !networkNodeIds[n.uid]);
// const nodes = [...network.nodes, ...externalNodes].sort((a, b) => a.uid.localeCompare(b.uid));
// const index = nodes.findIndex(n => n.uid === node.uid);
// const label = index > -1 ? `${node.name} ${index + 1}` : node.name;
//
// 4. Add emoji suffix based on a node property
// const label = `${node.name} ${node.close_friend ? '😇' : '😡'}`;
// console.log('worker req', label);
// setTimeout(() => postMessage({ node, label }), 1000 * Math.random());

// 5. Add emoji based on an edge or node property
let label = node.name;
if (network.edges.some(e => e.from === node.id || e.to === node.id)) {
label += '😎';
} else if (node.close_friend) {
label += '😇';
}

// To perform async work, return a promise instead.
// Use this with care; the most recent response to the client will be used.
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// resolve(label);
// }, 100);
// });

return label;
}
54 changes: 29 additions & 25 deletions src/components/Node.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,54 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getExternalData, makeGetNodeColor } from '../selectors/protocol';
import { makeGetNodeColor } from '../selectors/protocol';
import { Node as UINode } from '../ui/components';

// TODO: using directly here; remove from parents?
import WorkerAgent from '../utils/WorkerAgent';
import { getNetwork, getNodeLabelFunction } from '../selectors/interface';

const getNodeColor = makeGetNodeColor();

/**
* Renders a Node.
*/

class Node extends PureComponent {
constructor(props) {
super(props);
this.state = {};
// On prompt change, is ready immediately
this.initWorker(props.workerUrl);
}

componentDidUpdate() {
this.initWorker(this.props.workerUrl);
componentDidUpdate(nextProps, nextState) {
if (this.state.label === nextState.label) {
// Unless the label has changed, we need to check for an update
this.initWorker(this.props.workerUrl);
}
}

componentWillUnmount() {
if (this.webWorker && this.outstandingMessage) {
this.webWorker.cancelMessage(this.outstandingMessage);
}
}

initWorker(url) {
if (url && !this.webWorker) {
this.webWorker = new Worker(url);
this.webWorker.onerror = err => this.setState({ workerError: err });
this.webWorker.onmessage = evt => this.setState({ label: evt.data });
// TODO: fix prop commingling
const node = { ...this.props, dispatch: null, getLabel: null, workerUrl: null };
this.webWorker.postMessage({
node,
network: this.props.workerNetwork || {},
externalData: this.props.workerExternalData || {},
});
if (!url || this.state.workerError) {
return;
}
if (!this.webWorker) {
this.webWorker = new WorkerAgent(url);
}
// TODO: fix prop commingling (#596); this shouldn't be needed
const node = { ...this.props, dispatch: null, getLabel: null, workerUrl: null };
const msgPromise = this.webWorker.sendMessageAsync({
node,
network: this.props.workerNetwork || {},
});
this.outstandingMessage = msgPromise.cancellationId;
msgPromise
.then(label => this.setState({ label }))
.catch(workerError => this.setState({ workerError }));
}

render() {
Expand All @@ -48,7 +59,6 @@ class Node extends PureComponent {

const useWorkerLabel = workerUrl !== false && !this.state.workerError;
const label = useWorkerLabel ? (this.state.label || '') : this.props.getLabel(this.props);

return (
<UINode
color={color}
Expand All @@ -63,30 +73,24 @@ function mapStateToProps(state, props) {
return {
color: getNodeColor(state, props),
getLabel: getNodeLabelFunction(state),

// TODO: external selector
workerUrl: state.protocol.workerUrl,
workerNetwork: state.protocol.workerUrl && getNetwork(state),
workerExternalData: state.protocol.workerUrl && getExternalData(state),
workerNetwork: (state.protocol.workerUrl && getNetwork(state)) || null,
};
}

Node.propTypes = {
type: PropTypes.string.isRequired,
color: PropTypes.string,
getLabel: PropTypes.func.isRequired,

workerUrl: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
workerNetwork: PropTypes.object,
workerExternalData: PropTypes.object,
};

Node.defaultProps = {
color: 'node-color-seq-1',
workerUrl: undefined,
workerNetwork: null,
workerVariableRegistry: null,
workerExternalData: null,
};

export default connect(mapStateToProps)(Node);
15 changes: 1 addition & 14 deletions src/containers/ConcentricCircles/LayoutNode.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { Node } from '../../components';
import { getNodeLabelFunction } from '../../selectors/interface';
import { selectable } from '../../behaviours';
import { DragSource } from '../../behaviours/DragAndDrop';
import { NO_SCROLL } from '../../behaviours/DragAndDrop/DragManager';
Expand All @@ -18,7 +16,6 @@ class LayoutNode extends PureComponent {
const {
allowPositioning,
allowSelect,
getLabel,
layoutVariable,
linking,
node,
Expand All @@ -39,7 +36,6 @@ class LayoutNode extends PureComponent {
style={styles}
>
<EnhancedNode
label={getLabel(node)}
onSelected={this.props.onSelected}
selected={selected}
linking={linking}
Expand All @@ -60,7 +56,6 @@ LayoutNode.propTypes = {
allowSelect: PropTypes.bool,
areaHeight: PropTypes.number,
areaWidth: PropTypes.number,
getLabel: PropTypes.func.isRequired,
layoutVariable: PropTypes.string.isRequired,
linking: PropTypes.bool,
node: PropTypes.object.isRequired,
Expand All @@ -78,14 +73,6 @@ LayoutNode.defaultProps = {
selected: false,
};

function mapStateToProps(state) {
return {
getLabel: getNodeLabelFunction(state),
};
}

export { LayoutNode };

export default compose(
connect(mapStateToProps),
)(LayoutNode);
export default LayoutNode;
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ShallowWrapper {
"nodeType": "host",
"props": Object {
"children": Array [
<Connect(LayoutNode)
<LayoutNode
allowPositioning={false}
allowSelect={true}
areaHeight={456}
Expand Down Expand Up @@ -110,7 +110,7 @@ ShallowWrapper {
"nodeType": "host",
"props": Object {
"children": Array [
<Connect(LayoutNode)
<LayoutNode
allowPositioning={false}
allowSelect={true}
areaHeight={456}
Expand Down
6 changes: 1 addition & 5 deletions src/containers/Interfaces/NameGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { get, has } from 'lodash';
import withPrompt from '../../behaviours/withPrompt';
import { actionCreators as sessionsActions } from '../../ducks/modules/sessions';
import { actionCreators as modalActions } from '../../ducks/modules/modals';
import { getNodeLabelFunction, makeNetworkNodesForPrompt } from '../../selectors/interface';
import { makeNetworkNodesForPrompt } from '../../selectors/interface';
import { makeGetPromptNodeAttributes } from '../../selectors/name-generator';
import { PromptSwiper, NodePanels, NodeForm } from '../';
import { NodeList, NodeBin } from '../../components/';
Expand Down Expand Up @@ -80,7 +80,6 @@ class NameGenerator extends Component {
render() {
const {
form,
getLabel,
nodesForPrompt,
openModal,
prompt,
Expand Down Expand Up @@ -138,7 +137,6 @@ class NameGenerator extends Component {
<div className="name-generator-interface__nodes">
<NodeList
nodes={nodesForPrompt}
label={getLabel}
listId={`${stage.id}_${prompt.id}_MAIN_NODE_LIST`}
id={'MAIN_NODE_LIST'}
accepts={({ meta }) => get(meta, 'itemType', null) === 'NEW_NODE'}
Expand Down Expand Up @@ -168,7 +166,6 @@ NameGenerator.propTypes = {
activePromptAttributes: PropTypes.object,
addNodes: PropTypes.func.isRequired,
form: PropTypes.object,
getLabel: PropTypes.func.isRequired,
newNodeAttributes: PropTypes.object.isRequired,
nodesForPrompt: PropTypes.array.isRequired,
openModal: PropTypes.func.isRequired,
Expand All @@ -188,7 +185,6 @@ function makeMapStateToProps() {
return {
activePromptAttributes: props.prompt.additionalAttributes,
form: rehydrateForm(state, props),
getLabel: getNodeLabelFunction(state),
newNodeAttributes: getPromptNodeAttributes(state, props),
nodesForPrompt: networkNodesForPrompt(state, props),
};
Expand Down
6 changes: 1 addition & 5 deletions src/containers/Interfaces/OrdinalBin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import withPrompt from '../../behaviours/withPrompt';
import { PromptSwiper, OrdinalBins } from '../';
import { getNodeLabelFunction, makeGetPromptVariable, makeNetworkNodesForSubject } from '../../selectors/interface';
import { makeGetPromptVariable, makeNetworkNodesForSubject } from '../../selectors/interface';
import { OrdinalBinBucket } from '../../components';
import { actionCreators as sessionsActions } from '../../ducks/modules/sessions';

Expand All @@ -18,7 +18,6 @@ class OrdinalBin extends Component {
promptForward,
promptBackward,
prompt,
getLabel,
nodesForPrompt,
stage,
} = this.props;
Expand All @@ -41,7 +40,6 @@ class OrdinalBin extends Component {
<OrdinalBinBucket
nodes={nodesForPrompt}
listId={`${stage.id}_${prompt.id}_NODE_BUCKET`}
label={getLabel}
id={'NODE_BUCKET'}
itemType="EXISTING_NODE"
onDrop={this.onDrop}
Expand All @@ -59,7 +57,6 @@ class OrdinalBin extends Component {
OrdinalBin.propTypes = {
stage: PropTypes.object.isRequired,
prompt: PropTypes.object.isRequired,
getLabel: PropTypes.func.isRequired,
promptForward: PropTypes.func.isRequired,
promptBackward: PropTypes.func.isRequired,
nodesForPrompt: PropTypes.array.isRequired,
Expand All @@ -77,7 +74,6 @@ function makeMapStateToProps() {
nodesForPrompt: stageNodes.filter(
node => !node[activePromptVariable],
),
getLabel: getNodeLabelFunction(state),
};
};
}
Expand Down
6 changes: 1 addition & 5 deletions src/containers/NodeBucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { compose } from 'recompose';
import { Node } from '../components';
import { makeGetNextUnplacedNode, makeGetSociogramOptions } from '../selectors/sociogram';
import { getNodeLabelFunction } from '../selectors/interface';
import { DragSource } from '../behaviours/DragAndDrop';
import { NO_SCROLL } from '../behaviours/DragAndDrop/DragManager';

Expand All @@ -13,7 +12,6 @@ const EnhancedNode = DragSource(Node);
class NodeBucket extends PureComponent {
static propTypes = {
allowPositioning: PropTypes.bool,
getLabel: PropTypes.func.isRequired,
node: PropTypes.object,
};

Expand All @@ -25,7 +23,6 @@ class NodeBucket extends PureComponent {
render() {
const {
allowPositioning,
getLabel,
node,
} = this.props;

Expand All @@ -35,7 +32,7 @@ class NodeBucket extends PureComponent {
<div className="node-bucket">
{ node &&
<EnhancedNode
label={getLabel(node)}
key={node.uid}
meta={() => ({ ...node, itemType: 'POSITIONED_NODE' })}
scrollDirection={NO_SCROLL}
{...node}
Expand All @@ -52,7 +49,6 @@ function makeMapStateToProps() {

return function mapStateToProps(state, props) {
return {
getLabel: getNodeLabelFunction(state),
node: getNextUnplacedNode(state, props),
...getSociogramOptions(state, props),
};
Expand Down
Loading

0 comments on commit dd2aebc

Please sign in to comment.