Files
OpenNoodl/dev-docs/tasks/phase-3-editor-ux-overhaul/TASK-008-critical-runtime-bugs/SUBTASK-B-node-output-debugging.md
Richard Osborne 554dd9f3b4 feat(blockly): Phase A foundation - Blockly setup, custom blocks, and generators
- 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
2026-01-11 13:30:13 +01:00

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

  1. Expression Node (packages/noodl-runtime/src/nodes/std-library/expression.js)
  2. 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:

  1. 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
  2. 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

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 📊

  1. Add all debug logging above
  2. Run npm run dev to start editor
  3. Create test nodes (Expression and Function)
  4. Watch console output
  5. 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 - scheduleAfterInputsHaveUpdated implementation
  • Verify this._afterInputsHaveUpdatedCallbacks array exists
  • Check if _performDirtyUpdate is 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 Node class flagOutputDirty() 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._internal exists 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:

  1. Implement targeted fix
  2. Remove debug logging (or make conditional)
  3. Test thoroughly
  4. Document fix in INVESTIGATION.md
  5. 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 + 1 outputs 2
  • With inputs: Connect Number node to x, expression x * 2 outputs correct value
  • With signals: Connect Run signal, expression evaluates on trigger
  • With Noodl globals: Variables.myVar outputs correct value
  • Signal outputs: isTrueEv fires when result is truthy
  • Multiple connected outputs: Both result and asString work

Function Node Tests

  • Simple output: Outputs.result = 42 outputs 42
  • Multiple outputs: Multiple Outputs.x = ... assignments all work
  • Signal outputs: Outputs.done.send() triggers correctly
  • With inputs: Access Inputs.x and output based on it
  • Async functions: async functions 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

Core:

  • packages/noodl-runtime/src/node.js - Base Node class
  • packages/noodl-runtime/src/nodecontext.js - Node context/scope
  • packages/noodl-runtime/src/nodes/std-library/expression.js - Expression node
  • packages/noodl-runtime/src/nodes/std-library/simplejavascript.js - Function node

Related Systems:

  • packages/noodl-runtime/src/expression-evaluator.js - Expression evaluation
  • packages/noodl-runtime/src/outputproperty.js - Output handling
  • packages/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:

  1. Compare with a known-working node type (e.g., Number node)
  2. Check git history for recent changes to affected files
  3. Test in older version to see if regression
  4. Ask Richard about recent runtime changes
  5. 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;