Hex Crown
Hex Crown

Reputation: 753

JavaScript generic Async/Await "debounce"

So basically I've got an async event callback that sometimes fires off quite quickly. What Im looking to do is to execute some of the method right then, but then wait until a while after the last even call before executing the final bit of the code.

here is an example:

let debounceTimeout
async function onEvent() {
    // do stuff here on every event

    const waitTime = 5000
    await new Promise(resolve => {
        clearTimeout(debounceTimeout)
        debounceTimeout = setTimeout(resolve, waitTime)
    })

    // do stuff here only on the last event after waiting for "waitTime"
}

so the above example works exactly how I want however, to the keen eyed, you may realize everything before the final event is being "awaited" indefinitely which (I'm assuming) is a memory leak as it will keep creating new promises that never resolve.

Basically I'm wondering if there is any generic way to do a denounce that is the same functionally but without the memory leak. bonus points if it can be cleaned up somehow into a simple await debounce(timeInMS) call.

P.S. I was thinking maybe something along the lines of rejecting timeouts that will never be resolved, but I'm not sure if that would be a good approach.

P.S.S. I know what I'm asking can effectively be done by keeping track of the events and checking if a new one has occurred after waiting the 5 seconds on that one, and if I need to go that way, so be it. That said this will be a common pattern in the application I'm working on and I'd like something a little cleaner, and was hoping this idea wasn't to much of a stretch.

Upvotes: 1

Views: 2439

Answers (1)

trincot
trincot

Reputation: 350147

For this use case promises are not the ideal mechanism:

  1. Event triggering code is usually not expecting a promise to be returned. It just broadcasts.

  2. You would need to "cancel" a promise when a new event arrives before the waiting time expires. This you can do by resolving or rejecting that particular promise, but then you must still differentiate that outcome from a normal resolution of that promise. The code needed for that seems less elegant. But I'll let you be the judge of that (see below)

  3. setTimeout on its own seems to do the job well enough already

Below two alternatives for the same demo. It triggers events with random intervals. The output shows a dot for every one of them. When the waiting timeout expires before the next event comes in, a new line is started in the output:

Demo using promises

const waitTime = 700;

async function onEvent() {
    // do stuff here on every event
    log.textContent = "." + log.textContent;
    
    if (onEvent.resolve) onEvent.resolve(true);
    
    if (await new Promise(resolve => {
        onEvent.resolve = resolve; // Get a handle to resolve this promise preemptively
        setTimeout(resolve, waitTime);
    })) return; // Promise was resolved before timeout happened

    // Do stuff here only on the last event after waiting for "waitTime"
    log.textContent = "\n" + log.textContent.replace(/\n|$/, "completed\n");
}

// Demo
setRandomInterval(onEvent, 1, 1000);

// Utility function for triggering an event at irregular intervals
function setRandomInterval(cb, min, max) {
    let timeout = setTimeout(() => {
        cb();
        timeout = setRandomInterval(cb, min, max);
    }, min + Math.random()*(max-min));
    return () => clearTimeout(timeout);
}
<pre id="log"></pre>

Demo without promises

const waitTime = 700;

function onEvent() {
    // do stuff here on every event
    log.textContent = "." + log.textContent;
    
    clearTimeout(onEvent.debounceTimeout);
    onEvent.debounceTimeout = setTimeout(() => {
        // Do stuff here only on the last event after waiting for "waitTime"
        log.textContent = "\n" + log.textContent.replace(/\n|$/, "completed\n");
    }, waitTime);
}

// Demo
setRandomInterval(onEvent, 1, 1000);

// Utility function for triggering an event at irregular intervals
function setRandomInterval(cb, min, max) {
    let timeout = setTimeout(() => {
        cb();
        timeout = setRandomInterval(cb, min, max);
    }, min + Math.random()*(max-min));
    return () => clearTimeout(timeout);
}
<pre id="log"></pre>

Upvotes: 4

Related Questions