The Debounce Function - a Savior

·

3 min read

In a project I'm working on, part of our decision-making logic uses data from a PLC (Programmable Logic Controller). Without getting very technical, the PLC communication interface we use (Node OPCUA) asks the PLC to monitor certain tags, i.e., we get notified anytime the value changes.

The Problem

Now, one piece of information that we use in the application is a 17-character long string - each character has a separate tag, so anytime the string gets updated, 17 or more change events may be triggered.

The Code

monitoredItemGroup is one set of 17 character strings. There are 150 more monitoredItemGroup variables since this is a snippet from a for loop. The logic is as follows:

  • Watch the monitoredItemGroup
  • When something changes, read another PLC tag
  • Use the result to run some more logic (some may involve more PLC tags)

      // function that reads a PLC tag
      const findValue = (station, session) => {
        session.read(
          { nodeId: `${station}.PLCTag`, attributeId: AttributeIds.Value },
          (err, dataValue) => {
            if (!err) {
              const tagValue = dataValue.value.value.toString();
    
              anotherFunction(session, station, tagValue, socket);
            }
          }
        );
      };
    
      // watches for change event
      monitoredItemGroup.on('changed', (monitoredItem, dataValue, index) => {
          // some logic
          // ...
          findValue(station, session)
      })
    

This is clearly not ideal at all. Why, you ask?

Well, anytime the 17 character string changes, we have a hook that runs some logic on our backend. If the string changes over 17 times (one change event per character), we'd trigger the hook way more than 17+ times - and this ain't good at all.

So, I needed a better way to do this - what if we could "wait" for the change events to finish occurring and then initiate the hook? That's exactly where a debounce function can help us.

The Solution

If you want the code and that's all you're here for - sure, here you go:

// Source - https://underscorejs.org/#debounce
function debounce(func, wait, immediate) {
    var timeout;
    return function () {
        var context = this, args = arguments;
        var later = function () {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

Here's a link to the function as a GitHub Gist if you'd like to share it.

What It Does

The debounce function takes in 3 arguments:

  • A function func
  • An integer wait that waits for wait milliseconds before executing func
  • A boolean immediate:
    • Set to true if you want to execute wait milliseconds after the first call to func
    • Set to false if you want to execute wait milliseconds after the last call to func

How to Implement It

After defining the debounce function globally, I 'wrapped' the findValue function in the debounce function:

const findValue = debounce((station, session) => {
      session.read(
        { nodeId: `${station}.PLCTag`, attributeId: AttributeIds.Value },
        (err, dataValue) => {
          if (!err) {
            const tagValue = dataValue.value.value.toString();

            anotherFunction(session, station, tagValue, socket);
          }
        }
      );
    }, 250);

    // watch `monitoredItemGroup` code

This ensures that, regardless of how many times findValue is called, it runs only the last time it's called, i.e., if it's not called again within the 250ms. As I didn't explicitly set immediate, it's considered a falsy value, i.e., it's undefined, thus false (very simplified, but read the MDN docs to learn more).

Possible Use Cases

Aside from the situation I encountered, the debounce function can also prevent:

  • Submitting a payment twice (stonks)
  • Hitting an API rate limit (when your users wanna mess with you)
  • Expensive DOM events - e.g. update something anytime a user scrolls
  • Excessively auto-saving / autocompleting something (speed is good, but practicality is prolly better)