- Install blockly package (~500KB) - Create BlocklyWorkspace React component with serialization - Define custom Noodl blocks (Input/Output, Variables, Objects, Arrays) - Implement JavaScript code generators for all custom blocks - Add theme-aware styling for Blockly workspace - Export initialization functions for easy integration Part of TASK-012: Blockly Visual Logic Integration
12 KiB
SUBTASK-B: Debug & Fix Expression/Function Node Outputs
Parent Task: TASK-008
Status: 🔴 Not Started
Priority: CRITICAL
Estimated Effort: 3-5 hours
Problem
Expression and Function nodes evaluate/run but don't send output data to connected downstream nodes, breaking core functionality.
Affected Nodes
- Expression Node (
packages/noodl-runtime/src/nodes/std-library/expression.js) - Function Node (
packages/noodl-runtime/src/nodes/std-library/simplejavascript.js)
Both nodes share similar output mechanisms, suggesting a common underlying issue.
Investigation Strategy
This is a debugging task - the root cause is unknown. We'll use systematic investigation to narrow down the issue.
Phase 1: Minimal Reproduction 🔍
Create the simplest possible test case:
-
Expression Node Test:
- Create Expression node with
1 + 1 - Connect output to Text node
- Expected: Text shows "2"
- Actual: Text shows nothing or old value
- Create Expression node with
-
Function Node Test:
- Create Function node with
Outputs.result = 42; - Connect output to Text node
- Expected: Text shows "42"
- Actual: Text shows nothing or old value
- Create Function node with
Document:
- Exact steps to reproduce
- Screenshots of node graph
- Console output
- Any error messages
Phase 2: Add Debug Logging 🔬
Add strategic console.log statements to trace execution flow.
Expression Node Logging
File: packages/noodl-runtime/src/nodes/std-library/expression.js
Location 1 - Input Change:
// Line ~50, in expression input set()
set: function (value) {
console.log('🟢 [Expression] Input changed:', value);
var internal = this._internal;
internal.currentExpression = functionPreamble + 'return (' + value + ');';
// ... rest of code
if (!this.isInputConnected('run')) this._scheduleEvaluateExpression();
}
Location 2 - Schedule:
// Line ~220, _scheduleEvaluateExpression
_scheduleEvaluateExpression: {
value: function () {
console.log('🔵 [Expression] Schedule evaluation called');
var internal = this._internal;
if (internal.hasScheduledEvaluation === false) {
console.log('🔵 [Expression] Scheduling callback');
internal.hasScheduledEvaluation = true;
this.flagDirty();
this.scheduleAfterInputsHaveUpdated(function () {
console.log('🔥 [Expression] Callback FIRED');
var lastValue = internal.cachedValue;
internal.cachedValue = this._calculateExpression();
console.log('🔥 [Expression] Calculated:', internal.cachedValue, 'Previous:', lastValue);
if (lastValue !== internal.cachedValue) {
console.log('🚩 [Expression] Flagging outputs dirty');
this.flagOutputDirty('result');
this.flagOutputDirty('isTrue');
this.flagOutputDirty('isFalse');
}
if (internal.cachedValue) this.sendSignalOnOutput('isTrueEv');
else this.sendSignalOnOutput('isFalseEv');
internal.hasScheduledEvaluation = false;
});
} else {
console.log('⚠️ [Expression] Already scheduled, skipping');
}
}
}
Location 3 - Output Getter:
// Line ~145, result output getter
result: {
group: 'Result',
type: '*',
displayName: 'Result',
getter: function () {
console.log('📤 [Expression] Result getter called, returning:', this._internal.cachedValue);
if (!this._internal.currentExpression) {
return 0;
}
return this._internal.cachedValue;
}
}
Function Node Logging
File: packages/noodl-runtime/src/nodes/std-library/simplejavascript.js
Location 1 - Schedule:
// Line ~100, scheduleRun method
scheduleRun: function () {
console.log('🔵 [Function] Schedule run called');
if (this.runScheduled) {
console.log('⚠️ [Function] Already scheduled, skipping');
return;
}
this.runScheduled = true;
this.scheduleAfterInputsHaveUpdated(() => {
console.log('🔥 [Function] Callback FIRED');
this.runScheduled = false;
if (!this._deleted) {
this.runScript();
}
});
}
Location 2 - Proxy:
// Line ~25, Proxy set trap
this._internal.outputValuesProxy = new Proxy(this._internal.outputValues, {
set: (obj, prop, value) => {
console.log('🔵 [Function] Proxy intercepted:', prop, '=', value);
//a function node can continue running after it has been deleted. E.g. with timeouts or event listeners that hasn't been removed.
//if the node is deleted, just do nothing
if (this._deleted) {
console.log('⚠️ [Function] Node deleted, ignoring output');
return;
}
//only send outputs when they change.
//Some Noodl projects rely on this behavior, so changing it breaks backwards compability
if (value !== this._internal.outputValues[prop]) {
console.log('🚩 [Function] Flagging output dirty:', 'out-' + prop);
this.registerOutputIfNeeded('out-' + prop);
this._internal.outputValues[prop] = value;
this.flagOutputDirty('out-' + prop);
} else {
console.log('⏭️ [Function] Output unchanged, skipping');
}
return true;
}
});
Location 3 - Output Getter:
// Line ~185, getScriptOutputValue method
getScriptOutputValue: function (name) {
console.log('📤 [Function] Output getter called:', name, 'value:', this._internal.outputValues[name]);
if (this._isSignalType(name)) {
return undefined;
}
return this._internal.outputValues[name];
}
Phase 3: Test with Logging 📊
- Add all debug logging above
- Run
npm run devto start editor - Create test nodes (Expression and Function)
- Watch console output
- Document what logs appear and what logs are missing
Expected Log Flow (Expression):
🟢 [Expression] Input changed: 1 + 1
🔵 [Expression] Schedule evaluation called
🔵 [Expression] Scheduling callback
🔥 [Expression] Callback FIRED
🔥 [Expression] Calculated: 2 Previous: 0
🚩 [Expression] Flagging outputs dirty
📤 [Expression] Result getter called, returning: 2
Expected Log Flow (Function):
🔵 [Function] Schedule run called
🔥 [Function] Callback FIRED
🔵 [Function] Proxy intercepted: result = 42
🚩 [Function] Flagging output dirty: out-result
📤 [Function] Output getter called: result value: 42
If logs stop at certain point, that's where the bug is.
Phase 4: Narrow Down Root Cause 🎯
Based on Phase 3 findings, investigate specific areas:
Scenario A: Callback Never Fires
Symptoms:
- See "Schedule" logs but never see "Callback FIRED"
scheduleAfterInputsHaveUpdated()not working
Investigation:
- Check
packages/noodl-runtime/src/node.js-scheduleAfterInputsHaveUpdatedimplementation - Verify
this._afterInputsHaveUpdatedCallbacksarray exists - Check if
_performDirtyUpdateis being called - Look for React 19 related changes that might have broken scheduling
Potential Fix:
- Fix scheduling mechanism
- Ensure callbacks are executed properly
Scenario B: Outputs Flagged But Getters Not Called
Symptoms:
- See "Flagging outputs dirty" logs
- Never see "Output getter called" logs
flagOutputDirty()works but doesn't trigger downstream updates
Investigation:
- Check base
NodeclassflagOutputDirty()implementation - Verify downstream nodes are checking for dirty outputs
- Check if connection system is broken
- Look for changes to output propagation mechanism
Potential Fix:
- Fix output propagation system
- Ensure getters are called when outputs are dirty
Scenario C: Context/Scope Missing
Symptoms:
- Expression compilation fails silently
- No errors in console but calculation returns 0 or undefined
Investigation:
- Add logging to check
this.context - Add logging to check
this.context.modelScope - Verify Noodl globals (Variables, Objects, Arrays) are accessible
Potential Fix:
- Ensure context is properly initialized
- Fix scope setup
Scenario D: Proxy Not Working (Function Only)
Symptoms:
- Function runs but Proxy set trap never fires
- Output assignments don't trigger updates
Investigation:
- Test if Proxy works in current Node.js version
- Check if
this._internalexists when Proxy is created - Verify Proxy is being used (not bypassed)
Potential Fix:
- Fix Proxy initialization
- Use alternative output mechanism if Proxy is broken
Phase 5: Implement Fix 🔧
Once root cause is identified:
- Implement targeted fix
- Remove debug logging (or make conditional)
- Test thoroughly
- Document fix in INVESTIGATION.md
- Add entry to LEARNINGS.md
Success Criteria
- Expression nodes output correct values to connected nodes
- Function nodes output correct values to connected nodes
- Signal outputs work correctly
- Reactive updates work (expression updates when inputs change)
- No console errors during evaluation
- Downstream nodes receive and display outputs
- Existing projects using these nodes work correctly
Testing Checklist
Expression Node Tests
- Simple math:
1 + 1outputs2 - With inputs: Connect Number node to
x, expressionx * 2outputs correct value - With signals: Connect Run signal, expression evaluates on trigger
- With Noodl globals:
Variables.myVaroutputs correct value - Signal outputs:
isTrueEvfires when result is truthy - Multiple connected outputs: Both
resultandasStringwork
Function Node Tests
- Simple output:
Outputs.result = 42outputs42 - Multiple outputs: Multiple
Outputs.x = ...assignments all work - Signal outputs:
Outputs.done.send()triggers correctly - With inputs: Access
Inputs.xand output based on it - Async functions:
asyncfunctions work correctly - Error handling: Errors don't crash editor, show in warnings
Integration Tests
- Chain: Expression → Function → Text all work
- Multiple connections: One output connected to multiple inputs
- Reactive updates: Changing upstream input updates downstream
- Component boundary: Nodes work inside components
Related Files
Core:
packages/noodl-runtime/src/node.js- Base Node classpackages/noodl-runtime/src/nodecontext.js- Node context/scopepackages/noodl-runtime/src/nodes/std-library/expression.js- Expression nodepackages/noodl-runtime/src/nodes/std-library/simplejavascript.js- Function node
Related Systems:
packages/noodl-runtime/src/expression-evaluator.js- Expression evaluationpackages/noodl-runtime/src/outputproperty.js- Output handlingpackages/noodl-runtime/src/nodegraphcontext.js- Graph-level context
Notes
- This is investigation-heavy - expect to spend time debugging
- Root cause may affect other node types
- May uncover deeper runtime issues
- Document all findings thoroughly
- Consider adding automated tests for these nodes once fixed
- If fix is complex, consider creating separate LEARNINGS entry
Debugging Tips
If stuck:
- Compare with a known-working node type (e.g., Number node)
- Check git history for recent changes to affected files
- Test in older version to see if regression
- Ask Richard about recent runtime changes
- Check if similar issues reported in GitHub issues
Useful console commands:
// Get node instance
const node = window.Noodl.Graphs['Component'].nodes[0];
// Check outputs
node._internal.cachedValue;
node._internal.outputValues;
// Test flagging manually
node.flagOutputDirty('result');
// Check scheduling
node._afterInputsHaveUpdatedCallbacks;