2022-07-20 19:06:34 +00:00
|
|
|
let all_nodes = new Set();
|
2022-07-18 07:34:29 +00:00
|
|
|
let node_debug_stack = [];
|
2022-07-20 19:06:34 +00:00
|
|
|
let COMPUTE_GRAPH_DEBUG = false;
|
2022-05-21 22:51:09 +00:00
|
|
|
class ComputeNode {
|
2022-05-22 07:14:20 +00:00
|
|
|
/**
|
2022-05-21 22:51:09 +00:00
|
|
|
* Make a generic compute node.
|
|
|
|
* Adds the node to the global map of nodenames to nodes (for calling from html listeners).
|
|
|
|
*
|
|
|
|
* @param name : Name of the node (string). Must be unique. Must "fit in" a JS string (terminated by single quotes).
|
|
|
|
*/
|
2022-05-14 06:01:03 +00:00
|
|
|
constructor(name) {
|
2022-06-19 07:42:49 +00:00
|
|
|
this.inputs = []; // parent nodes
|
2022-06-19 16:49:04 +00:00
|
|
|
this.input_translation = new Map();
|
2022-05-14 06:01:03 +00:00
|
|
|
this.children = [];
|
2022-06-19 16:49:04 +00:00
|
|
|
this.value = null;
|
2022-05-14 06:01:03 +00:00
|
|
|
this.name = name;
|
2022-05-21 22:51:09 +00:00
|
|
|
this.update_task = null;
|
2022-05-22 05:38:43 +00:00
|
|
|
this.fail_cb = false; // Set to true to force updates even if parent failed.
|
2022-07-01 05:22:15 +00:00
|
|
|
this.dirty = 2; // 3 states:
|
|
|
|
// 2: dirty
|
|
|
|
// 1: possibly dirty
|
|
|
|
// 0: clean
|
2022-06-19 07:42:49 +00:00
|
|
|
this.inputs_dirty = new Map();
|
|
|
|
this.inputs_dirty_count = 0;
|
2022-07-20 19:06:34 +00:00
|
|
|
if (COMPUTE_GRAPH_DEBUG) { all_nodes.add(this); }
|
2022-05-14 06:01:03 +00:00
|
|
|
}
|
|
|
|
|
2022-05-22 07:14:20 +00:00
|
|
|
/**
|
2022-05-22 02:14:00 +00:00
|
|
|
* Request update of this compute node. Pushes updates to children.
|
|
|
|
*/
|
2022-06-19 18:43:02 +00:00
|
|
|
update() {
|
2022-06-19 20:44:02 +00:00
|
|
|
if (this.inputs_dirty_count != 0) {
|
2022-05-21 22:51:09 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-07-01 05:22:15 +00:00
|
|
|
if (this.dirty === 0) {
|
2022-06-19 07:42:49 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-07-20 19:06:34 +00:00
|
|
|
if (COMPUTE_GRAPH_DEBUG) { node_debug_stack.push(this.name); }
|
2022-07-01 05:22:15 +00:00
|
|
|
if (this.dirty == 2) {
|
|
|
|
let calc_inputs = new Map();
|
|
|
|
for (const input of this.inputs) {
|
|
|
|
calc_inputs.set(this.input_translation.get(input.name), input.value);
|
|
|
|
}
|
|
|
|
this.value = this.compute_func(calc_inputs);
|
2022-06-19 07:42:49 +00:00
|
|
|
}
|
2022-07-01 05:22:15 +00:00
|
|
|
this.dirty = 0;
|
2022-06-19 07:42:49 +00:00
|
|
|
for (const child of this.children) {
|
2022-06-19 18:43:02 +00:00
|
|
|
child.mark_input_clean(this.name, this.value);
|
2022-06-19 07:42:49 +00:00
|
|
|
}
|
2022-07-20 19:06:34 +00:00
|
|
|
if (COMPUTE_GRAPH_DEBUG) { node_debug_stack.pop(); }
|
2022-06-23 08:25:19 +00:00
|
|
|
return this;
|
2022-05-22 10:21:34 +00:00
|
|
|
}
|
2022-05-21 22:51:09 +00:00
|
|
|
|
2022-05-22 10:21:34 +00:00
|
|
|
/**
|
2022-06-19 07:42:49 +00:00
|
|
|
* Mark parent as not dirty. Propagates calculation if all inputs are present.
|
2022-05-22 10:21:34 +00:00
|
|
|
*/
|
2022-06-19 18:43:02 +00:00
|
|
|
mark_input_clean(input_name, value) {
|
2022-06-19 07:42:49 +00:00
|
|
|
if (value !== null || this.fail_cb) {
|
|
|
|
if (this.inputs_dirty.get(input_name)) {
|
|
|
|
this.inputs_dirty.set(input_name, false);
|
|
|
|
this.inputs_dirty_count -= 1;
|
|
|
|
}
|
|
|
|
if (this.inputs_dirty_count === 0) {
|
2022-06-19 18:43:02 +00:00
|
|
|
this.update();
|
2022-05-22 05:38:43 +00:00
|
|
|
}
|
2022-05-14 06:01:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-19 07:42:49 +00:00
|
|
|
mark_input_dirty(input_name) {
|
|
|
|
if (!this.inputs_dirty.get(input_name)) {
|
|
|
|
this.inputs_dirty.set(input_name, true);
|
|
|
|
this.inputs_dirty_count += 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-01 05:22:15 +00:00
|
|
|
mark_dirty(dirty_state=2) {
|
|
|
|
if (this.dirty < dirty_state) {
|
|
|
|
this.dirty = dirty_state;
|
2022-06-19 07:42:49 +00:00
|
|
|
for (const child of this.children) {
|
|
|
|
child.mark_input_dirty(this.name);
|
2022-07-01 05:22:15 +00:00
|
|
|
child.mark_dirty(dirty_state);
|
2022-06-19 07:42:49 +00:00
|
|
|
}
|
|
|
|
}
|
2022-06-23 08:25:19 +00:00
|
|
|
return this;
|
2022-05-22 10:21:34 +00:00
|
|
|
}
|
|
|
|
|
2022-05-22 07:14:20 +00:00
|
|
|
/**
|
2022-05-22 02:14:00 +00:00
|
|
|
* Get value of this compute node. Can't trigger update cascades (push based update, not pull based.)
|
|
|
|
*/
|
2022-05-14 06:01:03 +00:00
|
|
|
get_value() {
|
|
|
|
return this.value
|
|
|
|
}
|
2022-05-22 02:14:00 +00:00
|
|
|
|
2022-05-22 07:14:20 +00:00
|
|
|
/**
|
2022-05-22 02:14:00 +00:00
|
|
|
* Abstract method for computing something. Return value is set into this.value
|
|
|
|
*/
|
2022-05-22 05:38:43 +00:00
|
|
|
compute_func(input_map) {
|
2022-05-22 02:14:00 +00:00
|
|
|
throw "no compute func specified";
|
|
|
|
}
|
2022-05-22 04:19:43 +00:00
|
|
|
|
2022-06-27 06:42:00 +00:00
|
|
|
/**
|
|
|
|
* Add link to a parent compute node, optionally with an alias.
|
|
|
|
*/
|
2022-06-19 16:49:04 +00:00
|
|
|
link_to(parent_node, link_name) {
|
2022-05-22 04:19:43 +00:00
|
|
|
this.inputs.push(parent_node)
|
2022-06-19 16:49:04 +00:00
|
|
|
link_name = (link_name !== undefined) ? link_name : parent_node.name;
|
|
|
|
this.input_translation.set(parent_node.name, link_name);
|
2022-06-25 05:43:28 +00:00
|
|
|
if (parent_node.dirty || (parent_node.value === null && !this.fail_cb)) {
|
2022-06-19 07:42:49 +00:00
|
|
|
this.inputs_dirty_count += 1;
|
2022-06-25 05:43:28 +00:00
|
|
|
this.inputs_dirty.set(parent_node.name, true);
|
2022-06-19 07:42:49 +00:00
|
|
|
}
|
2022-05-22 04:19:43 +00:00
|
|
|
parent_node.children.push(this);
|
2022-06-20 13:12:22 +00:00
|
|
|
return this;
|
2022-05-22 04:19:43 +00:00
|
|
|
}
|
2022-06-27 06:42:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a link to a parent node.
|
|
|
|
* TODO: time complexity of list deletion (not super relevant but it hurts my soul)
|
|
|
|
*/
|
|
|
|
remove_link(parent_node) {
|
|
|
|
const idx = this.inputs.indexOf(parent_node); // Get idx
|
|
|
|
this.inputs.splice(idx, 1); // remove element
|
|
|
|
|
2022-06-29 06:23:27 +00:00
|
|
|
this.input_translation.delete(parent_node.name);
|
2022-06-27 06:42:00 +00:00
|
|
|
const was_dirty = this.inputs_dirty.get(parent_node.name);
|
|
|
|
this.inputs_dirty.delete(parent_node.name);
|
|
|
|
if (was_dirty) {
|
|
|
|
this.inputs_dirty_count -= 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
const idx2 = parent_node.children.indexOf(this);
|
|
|
|
parent_node.children.splice(idx2, 1);
|
|
|
|
return this;
|
|
|
|
}
|
2022-05-14 06:01:03 +00:00
|
|
|
}
|
2022-05-21 22:51:09 +00:00
|
|
|
|
2022-07-01 05:22:15 +00:00
|
|
|
class ValueCheckComputeNode extends ComputeNode {
|
2022-07-25 15:58:41 +00:00
|
|
|
constructor(name) { super(name); this.valid_val = null; }
|
2022-07-01 05:22:15 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Request update of this compute node. Pushes updates to children,
|
|
|
|
* but only if this node's value changed.
|
|
|
|
*/
|
|
|
|
update() {
|
|
|
|
if (this.inputs_dirty_count != 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.dirty === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let calc_inputs = new Map();
|
|
|
|
for (const input of this.inputs) {
|
|
|
|
calc_inputs.set(this.input_translation.get(input.name), input.value);
|
|
|
|
}
|
|
|
|
let val = this.compute_func(calc_inputs);
|
2022-07-25 15:58:41 +00:00
|
|
|
if (val !== null) {
|
|
|
|
if (val !== this.valid_val) { super.mark_dirty(2); } // don't mark dirty if NULL (no update)
|
|
|
|
this.valid_val = val;
|
2022-07-01 05:22:15 +00:00
|
|
|
}
|
|
|
|
this.value = val;
|
|
|
|
this.dirty = 0;
|
|
|
|
for (const child of this.children) {
|
|
|
|
child.mark_input_clean(this.name, this.value);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defaulting to "dusty" state.
|
|
|
|
*/
|
|
|
|
mark_dirty(dirty_state="unused") {
|
|
|
|
return super.mark_dirty(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2022-07-18 07:34:29 +00:00
|
|
|
let graph_live_update = false;
|
2022-05-22 07:14:20 +00:00
|
|
|
/**
|
2022-05-21 22:51:09 +00:00
|
|
|
* Schedule a ComputeNode to be updated.
|
|
|
|
*
|
2022-05-22 07:57:47 +00:00
|
|
|
* @param node : ComputeNode to schedule an update for.
|
2022-05-21 22:51:09 +00:00
|
|
|
*/
|
2022-06-23 10:16:36 +00:00
|
|
|
function calcSchedule(node, timeout) {
|
2022-07-18 07:34:29 +00:00
|
|
|
if (!graph_live_update) return;
|
2022-05-21 22:51:09 +00:00
|
|
|
if (node.update_task !== null) {
|
|
|
|
clearTimeout(node.update_task);
|
|
|
|
}
|
2022-06-19 07:42:49 +00:00
|
|
|
node.mark_dirty();
|
2022-05-21 22:51:09 +00:00
|
|
|
node.update_task = setTimeout(function() {
|
2022-07-20 19:06:34 +00:00
|
|
|
if (COMPUTE_GRAPH_DEBUG) { node_debug_stack = []; }
|
2022-06-19 18:43:02 +00:00
|
|
|
node.update();
|
2022-05-21 22:51:09 +00:00
|
|
|
node.update_task = null;
|
2022-06-23 10:16:36 +00:00
|
|
|
}, timeout);
|
2022-05-21 22:51:09 +00:00
|
|
|
}
|
|
|
|
|
2022-05-22 10:21:34 +00:00
|
|
|
class PrintNode extends ComputeNode {
|
|
|
|
|
|
|
|
constructor(name) {
|
|
|
|
super(name);
|
|
|
|
this.fail_cb = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
compute_func(input_map) {
|
|
|
|
console.log([this.name, input_map]);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-19 07:42:49 +00:00
|
|
|
/**
|
|
|
|
* Node for getting an input from an input field.
|
2022-06-19 20:44:02 +00:00
|
|
|
* Fires updates whenever the input field is updated.
|
|
|
|
*
|
|
|
|
* Signature: InputNode() => str
|
2022-06-19 07:42:49 +00:00
|
|
|
*/
|
|
|
|
class InputNode extends ComputeNode {
|
|
|
|
constructor(name, input_field) {
|
|
|
|
super(name);
|
|
|
|
this.input_field = input_field;
|
2022-06-24 03:18:08 +00:00
|
|
|
this.input_field.addEventListener("input", () => calcSchedule(this, 500));
|
|
|
|
this.input_field.addEventListener("change", () => calcSchedule(this, 5));
|
2022-06-20 02:07:59 +00:00
|
|
|
//calcSchedule(this); Manually fire first update for better control
|
2022-06-19 07:42:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
compute_func(input_map) {
|
|
|
|
return this.input_field.value;
|
|
|
|
}
|
|
|
|
}
|
2022-06-29 06:23:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Passthrough node for simple aggregation.
|
|
|
|
* Unfortunately if you use this too much you get layers and layers of maps...
|
|
|
|
*
|
|
|
|
* Signature: PassThroughNode(**kwargs) => Map[...]
|
|
|
|
*/
|
|
|
|
class PassThroughNode extends ComputeNode {
|
|
|
|
constructor(name) {
|
|
|
|
super(name);
|
|
|
|
this.breakout_nodes = new Map();
|
|
|
|
}
|
|
|
|
|
|
|
|
compute_func(input_map) {
|
|
|
|
return input_map;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a ComputeNode that will "break out" one part of this aggregation input.
|
|
|
|
* There is some overhead to this operation because ComputeNode is not exactly a free abstraction... oof
|
|
|
|
* Also you will recv updates whenever any input that is part of the aggregation changes even
|
|
|
|
* if the specific sub-input didn't change.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* sub-input: The key to listen to
|
|
|
|
*/
|
|
|
|
get_node(sub_input) {
|
|
|
|
if (this.breakout_nodes.has(sub_input)) {
|
|
|
|
return this.breakout_nodes.get(sub_input);
|
|
|
|
}
|
|
|
|
const _name = this.name;
|
|
|
|
const ret = new (class extends ComputeNode {
|
|
|
|
constructor() { super('passthrough-'+_name+'-'+sub_input); }
|
|
|
|
compute_func(input_map) { return input_map.get(_name).get(sub_input); }
|
|
|
|
})().link_to(this);
|
|
|
|
this.breakout_nodes.set(sub_input, ret);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
}
|