Skip to content

Commit

Permalink
Merge pull request #20 from chrisjpatty/root-engine-circular-bailout
Browse files Browse the repository at this point in the history
🚀 RootEngine disallows circular dependencies
  • Loading branch information
chrisjpatty authored Aug 7, 2020
2 parents 1d10709 + f57af01 commit 38439ee
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 30 deletions.
4 changes: 4 additions & 0 deletions docs/docs/RootEngine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ Optional. If this property is set to `true`, the object returned by the `resolve
#### `rootNodeId` : string

Optional. In some cases you may wish to override which node is considered the root node. Passing the id to this property, of a node in the set of nodes passed into the `resolveRootNode` function, will cause the corresponding node to be used as the root node.

#### `maxLoops` : int

Optional. By default, the root engine will only allow up to `1000` recursive loops through nodes before it will bail out of processing the current root node input and log an error. This prevents the call stack from being overwhelmed by a circular node setup. Passing an integer to this property will change the number of max loops before this occurs. Passing a `-1` will disable this check and allow infinite loops.
51 changes: 27 additions & 24 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ flumeConfig
return (
<div
style={{
background: `linear-gradient(to right, ${inputData.multiColor.join(", ")})`,
background: `linear-gradient(to right, ${inputData.multiColor.join(
", "
)})`,
borderRadius: 4,
width: "100%",
height: 60
Expand Down Expand Up @@ -549,7 +551,7 @@ flumeConfig
inputs: ports => [ports.car(), ports.color()],
outputs: ports => [ports.car()]
})
.addNodeType({
.addRootNodeType({
type: "websiteAttributes",
label: "Website Attributes",
description: "Accepts the attributes of the website",
Expand All @@ -567,13 +569,9 @@ flumeConfig
.addNodeType({
type: "employee",
label: "Employee",
inputs: ports => [
ports.employeeType()
],
outputs: ports => [
ports.employeeType()
]
})
inputs: ports => [ports.employeeType()],
outputs: ports => [ports.employeeType()]
});

const engine = new RootEngine(
flumeConfig,
Expand Down Expand Up @@ -615,34 +613,39 @@ export default () => {
comments={comments}
onChange={setNodes}
onCommentsChange={setComments}
// disableZoom
// defaultNodes={[
// {
// type: "homePage",
// x: 400,
// y: -200
// }
// ]}
// debug
disableZoom
defaultNodes={[
{
type: "websiteAttributes",
x: 400,
y: -200
}
]}
debug
/>
<div style={{ marginTop: 30 }}>{/* <Website nodes={nodes} /> */}</div>
<div style={{ marginTop: 30 }}>
<Website nodes={nodes} />
</div>
</div>
);
};

const useInfiniteEngine = (nodes, engine, context, options = {}) =>
Object.keys(nodes).length ? engine.resolveRootNode(nodes, { context, ...options }) : {};

const Website = ({ nodes }) => {
const {
homepageTitle,
homepageSubtitle,
title,
description,
showDashboard,
showContactForm,
showLoginButton
} = useRootEngine(nodes, engine, { someContext: true });
} = useInfiniteEngine(nodes, engine, { someContext: true }, { maxLoops: 10 });

return (
<div className="website-wrapper">
<h1>{homepageTitle}</h1>
<p>{homepageSubtitle}</p>
<h1>{title}</h1>
<p>{description}</p>
{showDashboard && <div>Dashboard</div>}
{showContactForm && <div>Contact Form</div>}
{showLoginButton && <button>Login</button>}
Expand Down
53 changes: 47 additions & 6 deletions src/RootEngine.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
class LoopError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
static maxLoopsExceeded = 1;
}

class RootEngine {
constructor(config, resolveInputControls, fireNodeFunction) {
this.config = config;
this.fireNodeFunction = fireNodeFunction;
this.resolveInputControls = resolveInputControls;
this.loops = 0;
this.maxLoops = 1000;
}
resetLoops = maxLoops => {
this.maxLoops = maxLoops !== undefined ? maxLoops : 1000;
this.loops = 0;
};
checkLoops = () => {
if (this.maxLoops >= 0 && this.loops > this.maxLoops) {
throw new LoopError(
"Max loop count exceeded.",
LoopError.maxLoopsExceeded
);
} else {
this.loops++;
}
};
getRootNode = nodes => {
const roots = Object.values(nodes).filter(n => n.root);
if (roots.length > 1) {
Expand All @@ -23,7 +47,11 @@ class RootEngine {
return nodeType.inputs.reduce((obj, input) => {
const inputConnections = node.connections.inputs[input.name] || [];
if (inputConnections.length > 0) {
obj[input.name] = this.getValueOfConnection(inputConnections[0], nodes, context);
obj[input.name] = this.getValueOfConnection(
inputConnections[0],
nodes,
context
);
} else {
obj[input.name] = this.resolveInputControls(
input.type,
Expand All @@ -35,6 +63,7 @@ class RootEngine {
}, {});
};
getValueOfConnection = (connection, nodes, context) => {
this.checkLoops();
const outputNode = nodes[connection.nodeId];
const outputNodeType = this.config.nodeTypes[outputNode.type];
const inputValues = this.resolveInputValues(
Expand Down Expand Up @@ -70,14 +99,26 @@ class RootEngine {
const inputValues = this.reduceRootInputs(
rootNode.connections.inputs,
(inputName, connection) => {
return {
name: inputName,
value: this.getValueOfConnection(
this.resetLoops(options.maxLoops);
let value;
try {
value = this.getValueOfConnection(
connection[0],
nodes,
options.context
)
};
);
} catch (e) {
if (e.code === LoopError.maxLoopsExceeded) {
console.error(`${e.message} Circular nodes detected in ${inputName} port.`);
} else {
console.error(e)
}
} finally {
return {
name: inputName,
value
};
}
}
);
if (options.onlyResolveConnected) {
Expand Down

0 comments on commit 38439ee

Please sign in to comment.