The Debounce Function - a Savior
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 forwait
milliseconds before executingfunc
- A boolean
immediate
:- Set to
true
if you want to executewait
milliseconds after the first call tofunc
- Set to
false
if you want to executewait
milliseconds after the last call tofunc
- Set to
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)