workflow persistance

This commit is contained in:
RyanOnTheInside
2025-04-22 11:32:51 -04:00
parent 6fca42aafa
commit 2e0b867eff
+318 -1
View File
@@ -1,4 +1,66 @@
import { app } from "../../../scripts/app.js";
import { stateApi } from "./stateApi.js";
// Workflow namespace manager
const workflowManager = {
// Get the current workflow ID or generate a new one
getWorkflowId: function() {
if (!app.graph.extra) {
app.graph.extra = {};
}
if (!app.graph.extra.realtimeNodesState) {
// Generate a new workflow ID (timestamp + random)
const newId = `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
app.graph.extra.realtimeNodesState = { workflowId: newId };
app.graph.change(); // Ensure it gets saved
}
return app.graph.extra.realtimeNodesState.workflowId;
},
// Set a state value using the API
setState: function(key, value) {
const workflowId = this.getWorkflowId();
return stateApi.setValue(workflowId, key, value);
},
// Get a state value using the API
getState: function(key) {
const workflowId = this.getWorkflowId();
return stateApi.getValue(workflowId, key);
},
// Delete a state value using the API
deleteState: function(key) {
const workflowId = this.getWorkflowId();
return stateApi.deleteValue(workflowId, key);
},
// Get all state values for the current workflow
getAllState: function() {
const workflowId = this.getWorkflowId();
return stateApi.getAllValues(workflowId);
},
// Clear all state for the current workflow
clearAllState: function() {
const workflowId = this.getWorkflowId();
return stateApi.clearAllValues(workflowId);
},
// Create a namespaced key using the workflow ID (for display only)
getNamespacedKey: function(key) {
return `${this.getWorkflowId()}:${key}`;
},
// Extract the user-facing key from a namespaced key
extractUserKey: function(namespacedKey) {
if (!namespacedKey) return "";
const parts = namespacedKey.split(":");
return parts.length > 1 ? parts[1] : namespacedKey;
}
};
// Track all keys available
const stateKeys = new Set();
@@ -135,6 +197,17 @@ function updateAllGetNodesDropdowns(graph) {
});
}
// Helper to properly disable a widget
function disableWidget(widget) {
if (widget) {
widget.disabled = true;
widget.visibleWidth = 0;
widget.onMouseDown = () => {};
widget.onMouseMove = () => {};
widget.onMouseUp = () => {};
}
}
// Register the SetStateNode for UI customization
app.registerExtension({
name: "SetStateNode",
@@ -151,6 +224,19 @@ app.registerExtension({
// Store reference to the node
const node = this;
// Add hidden workflow_id widget
// Find if workflow_id widget already exists
let workflowIdWidget = node.widgets.find(w => w.name === "workflow_id");
if (!workflowIdWidget) {
const workflowId = workflowManager.getWorkflowId();
workflowIdWidget = node.addWidget("text", "workflow_id", workflowId, () => {}, {
serialize: false,
disabled: true
});
workflowIdWidget.hidden = true;
disableWidget(workflowIdWidget);
}
// Track key changes
const keyWidget = node.widgets.find(w => w.name === "key");
if (keyWidget) {
@@ -165,6 +251,9 @@ app.registerExtension({
stateKeys.add(value);
node.title = "Set State: " + value;
// Store the namespaced key in a hidden property
node._namespacedKey = workflowManager.getNamespacedKey(value);
// Update all GetStateNodes
if (node.graph) {
updateAllGetNodesDropdowns(node.graph);
@@ -176,6 +265,7 @@ app.registerExtension({
if (keyWidget.value && keyWidget.value.trim() !== "") {
stateKeys.add(keyWidget.value);
node.title = "Set State: " + keyWidget.value;
node._namespacedKey = workflowManager.getNamespacedKey(keyWidget.value);
}
}
@@ -237,6 +327,65 @@ app.registerExtension({
}
}
};
// Hook into the onExecuted event to ensure we're using the namespaced key
const onExecuted = node.onExecuted;
node.onExecuted = function(message) {
if (onExecuted) {
onExecuted.apply(this, arguments);
}
// Get key and value from the node
const keyWidget = node.widgets.find(w => w.name === "key");
const valueWidget = node.widgets.find(w => w.name === "value");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
// Save the value to the server-side state
const key = keyWidget.value;
const value = message?.output?.value; // Get value from execution result
// Use the state API to save the value
workflowManager.setState(key, value)
.then(() => {
console.log(`State value set for key: ${key}`);
})
.catch(error => {
console.error(`Error setting state value for key: ${key}`, error);
});
}
};
};
// Modify the serialize method to include the namespaced key
const onSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function(info) {
if (onSerialize) {
onSerialize.apply(this, arguments);
}
// Add the namespaced key to the serialized data
const keyWidget = this.widgets.find(w => w.name === "key");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
info.namespacedKey = workflowManager.getNamespacedKey(keyWidget.value);
}
};
// Modify the configure method to restore the namespaced key
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
if (onConfigure) {
onConfigure.apply(this, arguments);
}
// Restore the namespaced key from the serialized data
if (info.namespacedKey) {
this.properties.namespacedKey = info.namespacedKey;
} else {
const keyWidget = this.widgets.find(w => w.name === "key");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
this.properties.namespacedKey = workflowManager.getNamespacedKey(keyWidget.value);
}
}
};
}
});
@@ -257,6 +406,19 @@ app.registerExtension({
// Store reference to the node
const node = this;
// Add hidden workflow_id widget
// Find if workflow_id widget already exists
let workflowIdWidget = node.widgets.find(w => w.name === "workflow_id");
if (!workflowIdWidget) {
const workflowId = workflowManager.getWorkflowId();
workflowIdWidget = node.addWidget("text", "workflow_id", workflowId, () => {}, {
serialize: false,
disabled: true
});
workflowIdWidget.hidden = true;
disableWidget(workflowIdWidget);
}
// Set node appearance
this.color = "#2a363b";
this.bgcolor = "#3f5159";
@@ -275,8 +437,11 @@ app.registerExtension({
const newWidget = node.addWidget("combo", "key", keyValue, function(value) {
if (value) {
node.title = "Get State: " + value;
// Store the namespaced key in a hidden property
node._namespacedKey = workflowManager.getNamespacedKey(value);
} else {
node.title = "Get State";
node._namespacedKey = "";
}
}, {
values: () => [...stateKeys].sort(),
@@ -289,9 +454,10 @@ app.registerExtension({
node.widgets.splice(index, 0, removed);
}
// Initialize title
// Initialize title and namespaced key
if (newWidget.value) {
node.title = "Get State: " + newWidget.value;
node._namespacedKey = workflowManager.getNamespacedKey(newWidget.value);
} else {
node.title = "Get State";
}
@@ -353,6 +519,65 @@ app.registerExtension({
}
}
};
// Hook into the onExecuted event to ensure we're using the namespaced key
const onExecuted = node.onExecuted;
node.onExecuted = function(message) {
if (onExecuted) {
onExecuted.apply(this, arguments);
}
// Get key from the node
const keyWidget = node.widgets.find(w => w.name === "key");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
const key = keyWidget.value;
// Check if we need to refresh the state (for debugging)
if (message?.output !== undefined) {
// We could fetch the current value for validation
workflowManager.getState(key)
.then(value => {
console.log(`Retrieved state for key: ${key}`, value);
})
.catch(error => {
console.error(`Error retrieving state for key: ${key}`, error);
});
}
}
};
};
// Modify the serialize method to include the namespaced key
const onSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function(info) {
if (onSerialize) {
onSerialize.apply(this, arguments);
}
// Add the namespaced key to the serialized data
const keyWidget = this.widgets.find(w => w.name === "key");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
info.namespacedKey = workflowManager.getNamespacedKey(keyWidget.value);
}
};
// Modify the configure method to restore the namespaced key
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(info) {
if (onConfigure) {
onConfigure.apply(this, arguments);
}
// Restore the namespaced key from the serialized data
if (info.namespacedKey) {
this.properties.namespacedKey = info.namespacedKey;
} else {
const keyWidget = this.widgets.find(w => w.name === "key");
if (keyWidget && keyWidget.value && keyWidget.value.trim() !== "") {
this.properties.namespacedKey = workflowManager.getNamespacedKey(keyWidget.value);
}
}
};
}
});
@@ -406,4 +631,96 @@ app.registerExtension({
}
};
}
});
// Initialize workflow ID when graph is loaded or created
app.registerExtension({
name: "RealtimeNodesWorkflowID",
async setup() {
// Initialize workflow ID on startup
workflowManager.getWorkflowId();
// Helper function to update workflow_id in all state nodes
const updateWorkflowIdInNodes = () => {
const workflowId = workflowManager.getWorkflowId();
// Update all SetStateNodes
app.graph._nodes.forEach(node => {
if (node.type === "SetStateNode" || node.type === "GetStateNode") {
const workflowIdWidget = node.widgets.find(w => w.name === "workflow_id");
if (workflowIdWidget) {
workflowIdWidget.value = workflowId;
disableWidget(workflowIdWidget);
} else {
// Add the widget if it doesn't exist
const newWidget = node.addWidget("text", "workflow_id", workflowId, () => {}, {
serialize: false,
disabled: true
});
newWidget.hidden = true;
disableWidget(newWidget);
}
}
});
};
// Register graph observer to update nodes when graph changes
app.graph.onNodeAdded = function(node) {
if (node.type === "SetStateNode" || node.type === "GetStateNode") {
// Let the node creation process complete first
setTimeout(() => {
const workflowId = workflowManager.getWorkflowId();
const workflowIdWidget = node.widgets.find(w => w.name === "workflow_id");
if (workflowIdWidget) {
workflowIdWidget.value = workflowId;
disableWidget(workflowIdWidget);
}
}, 50);
}
};
// Hook into graph serialization to ensure workflow ID is set
const originalGraphToJSON = app.graph.toJSON;
app.graph.toJSON = function(data) {
// Ensure workflow ID is up to date before serialization
updateWorkflowIdInNodes();
// Call original method
return originalGraphToJSON.call(this, data);
};
},
async beforeClearGraph() {
// Generate a new ID when graph is cleared
if (app.graph.extra && app.graph.extra.realtimeNodesState) {
// Generate a new workflow ID for the new graph
const newId = `wf_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
app.graph.extra.realtimeNodesState.workflowId = newId;
}
},
async graphLoaded() {
// Make sure the workflow ID is established
// Use a small delay to ensure the graph is fully loaded
setTimeout(() => {
const workflowId = workflowManager.getWorkflowId();
// Update all state nodes with the current workflow ID
app.graph._nodes.forEach(node => {
if (node.type === "SetStateNode" || node.type === "GetStateNode") {
// Find or add the workflow_id widget
let workflowIdWidget = node.widgets.find(w => w.name === "workflow_id");
if (workflowIdWidget) {
workflowIdWidget.value = workflowId;
} else {
// Add the widget if it doesn't exist
const newWidget = node.addWidget("text", "workflow_id", workflowId, () => {}, {
serialize: false,
disabled: true
});
newWidget.hidden = true;
disableWidget(newWidget);
}
}
});
}, 100);
}
});