Rewrite as pure JavaScript frontend nodes - no trigger needed

- Nodes are now isVirtualNode = true (no Python execution)
- GetVariable uses getInputLink() to redirect to SetVariable's input
- Data flows directly through the graph, no execution order issues
- Get Variable has dropdown showing available variable names
- Node titles update to show variable name (Set: foo, Get: foo)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jessesep
2025-12-05 02:08:10 +01:00
co-authored by Claude
parent 7fa22d3ae9
commit d2a4ffdd3b
2 changed files with 176 additions and 206 deletions
+7 -165
View File
@@ -1,171 +1,13 @@
"""
SimpleVariables - Set and Get nodes for storing/retrieving any data by name in ComfyUI
SimpleVariables - Pure frontend Set/Get Variable nodes for ComfyUI
These nodes are implemented entirely in JavaScript (web/simplevariables.js).
They work by redirecting graph connections - no Python execution needed.
"""
import time
class AnyType(str):
"""A special type that bypasses ComfyUI's type checking - always matches any type."""
def __eq__(self, _):
return True
def __ne__(self, _):
return False
# Create the any type instance
ANY = AnyType("*")
# Global storage for variables
_variable_storage = {}
def get_preview_text(variable_name, value):
"""Generate preview text for a value"""
if value is None:
return f"{variable_name} = None"
type_name = type(value).__name__
if hasattr(value, 'shape'):
return f"{variable_name} = {type_name} {list(value.shape)}"
elif isinstance(value, str):
val_str = value[:50] + "..." if len(value) > 50 else value
return f'{variable_name} = "{val_str}"'
elif isinstance(value, (int, float, bool)):
return f"{variable_name} = {value}"
elif isinstance(value, list):
return f"{variable_name} = list[{len(value)}]"
elif isinstance(value, dict):
return f"{variable_name} = dict[{len(value)} keys]"
else:
return f"{variable_name} = {type_name}"
class SetVariable:
"""Store any data with a custom variable name - shows preview on node"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_var"}),
},
"optional": {
"value": (ANY,),
}
}
RETURN_TYPES = (ANY,)
RETURN_NAMES = ("value",)
FUNCTION = "set_var"
CATEGORY = "variables"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
def set_var(self, variable_name, value=None):
_variable_storage[variable_name] = value
preview = get_preview_text(variable_name, value)
return {"ui": {"text": [preview]}, "result": (value,)}
class GetVariable:
"""Retrieve data by variable name - shows preview on node
Connect SetVariable's output to 'trigger' to ensure proper execution order.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_var"}),
},
"optional": {
"trigger": (ANY,),
}
}
RETURN_TYPES = (ANY,)
RETURN_NAMES = ("value",)
FUNCTION = "get_var"
CATEGORY = "variables"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
def get_var(self, variable_name, trigger=None):
if variable_name not in _variable_storage:
available = list(_variable_storage.keys())
preview = f"{variable_name} = NOT SET (available: {available})"
return {"ui": {"text": [preview]}, "result": (None,)}
value = _variable_storage[variable_name]
preview = get_preview_text(variable_name, value)
return {"ui": {"text": [preview]}, "result": (value,)}
class ListVariables:
"""Show all stored variables on the node"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {}}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("variable_list",)
FUNCTION = "list_vars"
CATEGORY = "variables"
OUTPUT_NODE = True
def list_vars(self):
if not _variable_storage:
text = "(no variables set)"
else:
lines = [get_preview_text(name, value) for name, value in _variable_storage.items()]
text = "\n".join(lines)
return {"ui": {"text": [text]}, "result": (text,)}
class ClearVariables:
"""Clear all stored variables"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"clear": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("status",)
FUNCTION = "clear_vars"
CATEGORY = "variables"
OUTPUT_NODE = True
def clear_vars(self, clear):
if clear:
count = len(_variable_storage)
_variable_storage.clear()
status = f"Cleared {count} variable(s)"
else:
status = "Not cleared (set clear=True)"
return {"ui": {"text": [status]}, "result": (status,)}
NODE_CLASS_MAPPINGS = {
"SetVariable": SetVariable,
"GetVariable": GetVariable,
"ListVariables": ListVariables,
"ClearVariables": ClearVariables,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SetVariable": "Set Variable",
"GetVariable": "Get Variable",
"ListVariables": "List Variables",
"ClearVariables": "Clear Variables",
}
# No Python nodes - everything is in JavaScript
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+169 -41
View File
@@ -1,48 +1,176 @@
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
function displayText(nodeType, nodeData, appInstance) {
const onExecutedOriginal = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecutedOriginal?.apply(this, arguments);
// Pure frontend Set/Get Variable nodes - no Python execution needed
// Based on KJNodes SetGet implementation
if (message?.text) {
// Find or create text widget
let textWidget = this.widgets?.find(w => w.name === "display_text");
if (!textWidget) {
textWidget = ComfyWidgets.STRING(this, "display_text", [
"STRING", { multiline: true }
], appInstance).widget;
textWidget.inputEl.readOnly = true;
textWidget.inputEl.style.opacity = 0.7;
textWidget.inputEl.style.fontFamily = "monospace";
}
// Get text from message
let text = message.text;
if (Array.isArray(text)) {
text = text[0] || "";
}
textWidget.value = text;
// Resize node to fit
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) sz[0] = this.size[0];
if (sz[1] < this.size[1]) sz[1] = this.size[1];
this.onResize?.(sz);
appInstance.graph.setDirtyCanvas(true, false);
});
}
};
}
const LGraphNode = LiteGraph.LGraphNode;
app.registerExtension({
name: "SimpleVariables",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["SetVariable", "GetVariable", "ListVariables", "ClearVariables"].includes(nodeData.name)) {
displayText(nodeType, nodeData, app);
name: "SimpleVariables.SetVariable",
registerCustomNodes() {
class SetVariableNode extends LGraphNode {
constructor(title) {
super(title);
this.properties = this.properties || {};
this.properties.variableName = "";
// Variable name widget
this.addWidget("text", "name", "", (value) => {
this.properties.variableName = value;
this.title = value ? `Set: ${value}` : "Set Variable";
this.updateGetters();
});
// Input and passthrough output
this.addInput("value", "*");
this.addOutput("value", "*");
this.onConnectionsChange = (slotType, slot, isConnect, linkInfo) => {
if (slotType === 1 && this.inputs[0]) { // Input changed
if (isConnect && linkInfo) {
const type = this.getInputType(0);
if (type) {
this.inputs[0].type = type;
this.inputs[0].name = type;
this.outputs[0].type = type;
this.outputs[0].name = type;
}
} else if (!isConnect) {
this.inputs[0].type = "*";
this.inputs[0].name = "value";
this.outputs[0].type = "*";
this.outputs[0].name = "value";
}
this.updateGetters();
}
};
this.isVirtualNode = true;
}
getInputType(slot) {
const link = this.getInputLink(slot);
if (link) {
const originNode = this.graph._nodes_by_id[link.origin_id];
if (originNode && originNode.outputs[link.origin_slot]) {
return originNode.outputs[link.origin_slot].type;
}
}
return null;
}
updateGetters() {
if (!this.graph) return;
const name = this.properties.variableName;
this.graph._nodes.forEach(node => {
if (node.type === "GetVariableNode" && node.properties.variableName === name) {
node.syncWithSetter();
}
});
}
onRemoved() {
this.updateGetters();
}
}
},
LiteGraph.registerNodeType("SetVariableNode", Object.assign(SetVariableNode, {
title: "Set Variable",
}));
SetVariableNode.category = "variables";
}
});
app.registerExtension({
name: "SimpleVariables.GetVariable",
registerCustomNodes() {
class GetVariableNode extends LGraphNode {
constructor(title) {
super(title);
this.properties = this.properties || {};
this.properties.variableName = "";
// Variable name dropdown (shows available setters)
this.addWidget("combo", "name", "", (value) => {
this.properties.variableName = value;
this.title = value ? `Get: ${value}` : "Get Variable";
this.syncWithSetter();
}, {
values: () => this.getAvailableVariables()
});
this.addOutput("value", "*");
this.onConnectionsChange = () => {
this.validateLinks();
};
this.isVirtualNode = true;
}
getAvailableVariables() {
if (!this.graph) return [""];
const setters = this.graph._nodes.filter(n => n.type === "SetVariableNode");
const names = setters
.map(n => n.properties?.variableName || n.widgets?.[0]?.value || "")
.filter(n => n !== "")
.sort();
return ["", ...names];
}
findSetter() {
if (!this.graph || !this.properties.variableName) return null;
const name = this.properties.variableName;
return this.graph._nodes.find(n =>
n.type === "SetVariableNode" &&
(n.properties?.variableName === name || n.widgets?.[0]?.value === name)
);
}
syncWithSetter() {
const setter = this.findSetter();
if (setter && setter.inputs[0]) {
this.outputs[0].type = setter.inputs[0].type || "*";
this.outputs[0].name = setter.inputs[0].name || "value";
} else {
this.outputs[0].type = "*";
this.outputs[0].name = "value";
}
this.validateLinks();
}
validateLinks() {
if (!this.outputs[0].links || this.outputs[0].type === "*") return;
// Remove incompatible links
const linksToRemove = [];
this.outputs[0].links.forEach(linkId => {
const link = this.graph.links[linkId];
if (link && link.type !== "*" && link.type !== this.outputs[0].type) {
linksToRemove.push(linkId);
}
});
linksToRemove.forEach(id => this.graph.removeLink(id));
}
// This is the magic - redirect to the setter's input
getInputLink(slot) {
const setter = this.findSetter();
if (setter && setter.inputs[0]) {
return this.graph.links[setter.inputs[0].link];
}
return null;
}
onAdded() {
this.syncWithSetter();
}
}
LiteGraph.registerNodeType("GetVariableNode", Object.assign(GetVariableNode, {
title: "Get Variable",
}));
GetVariableNode.category = "variables";
}
});