3

Calling a function foo that does “something” after N milliseconds set by timeout. If foo is called again before timeout expires I first cancel timeout - then set new. Simple enough using a global variable for a timer.

Usually that is it, but then I found that in some cases I need to wait for the “something” to finish and then do some follow-up things. From that I assume a promise is the way to go.

Or in more general terms:

How to return a promise from a promise by a timeout that can be cancelled.


After some meddling I have this. Look at sample of how I use it with a second promise:

const promise_time = (ms, old) => {
    let timeout, p = { pending: true };
    if (old && old.pending)
        old.abort();
    p.wait = new Promise(resolve => {
        timeout = setTimeout(() => {
            p.pending = false;
            resolve();
        }, ms);
    });
    p.abort = () => {
        clearTimeout(timeout);
        p.pending = false;
    };
    return p;
};

Using it in combination with a second promise as shown below.

But it feels somewhat clumsy and overly complex; but I do not manage to see for myself if it is. Need fresh eyes :P.

Is there a better way to do this? (rather likely)

Have looked into AbortController but did not find a way that seemed more clean, rather the opposite.

Sample usage:

Below is a dummy example. Just type something in the text box. If keydown delay is < 1s the timeout is cancelled and new set. Else it fires and resolves promise.

The button is meant to resemble the normal procedure, i.e. just do it after a delay.

const promise_time = (ms, old) => {
    let timeout, p = { pending: true };
    if (old && old.pending)
        old.abort();
    p.wait = new Promise(resolve => {
        timeout = setTimeout(() => {
            p.pending = false;
            resolve();
        }, ms);
    });
    p.abort = () => {
        clearTimeout(timeout);
        p.pending = false;
    };
    return p;
};
let timed;
const do_timed_thing = v => {
    // Initiate timer + potentially clear old
    timed = promise_time(1000, timed);
    return new Promise(resolve => {
        timed.wait.then(() => {
            // garbageify object
            timed = null;
            add_li('timed val “' + v + '”');
            resolve();
        });
    });
};
const add_li = txt => {
    let li = document.createElement('LI');
    li.textContent = (new Date()).toLocaleTimeString() + ' ' + txt;
    document.getElementById('ul').appendChild(li);
};
const txt = document.getElementById('txt');
txt.addEventListener('input', () => {
    do_timed_thing(txt.value).then(() => {
        txt.value = '';
        add_li('cleared input');
    });
});
document.getElementById('nop')
.addEventListener('click',() => {
    do_timed_thing('Only something');
});
txt.focus();
<div style="display: grid">
<p>Max 1 sec between keystrokes; Do something + something else.</p>
<input id="txt" type="text" placeholder="Do something + something else" />
<hr />
<button id="nop">Do something (after 1 sec) and only that</button>
<ul id="ul"></ul>
</div>
Err488
  • 75
  • 4
  • interesting question – Tom Foster Oct 25 '21 at 16:01
  • 1
    This question of the kind that I (sadly) see more and more rarely on SO. It shows effort, is clear, well-formatted and on-topic (IMO). Keep up the good work! – FZs Oct 25 '21 at 21:17
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it) in `do_timed_thing`! – Bergi Oct 25 '21 at 22:04
  • Thanks for feedback. Believe I'll find a suitable solution trough the hints and links. The antipattern Q/A is a good read in the mix! – Err488 Oct 26 '21 at 23:20

1 Answers1

1

I would solve the problem as follows:


// An object that keeps track of the last action timeout and
// cancels it every time a new action gets planned.
const action_planner = {

    // This is the last scheduled timeout id.
    timeoutId: null, 
    
    setAction (callback, timeout) {

        // Cancel the previous action
        clearTimeout(this.timeoutId);

        // Replace it with the new action
        this.timeoutId = setTimeout(callback, timeout);
    }
}

element.addEventListener('click', () => action_panner.setAction(do_something, 1000));

Thanks to @FZs for the tip.

  • Creating and cancelling many timeout-promises with your design wouldn't cancel the underlying timeouts, that can affect performance negatively. It would also be easier to keep track of the timer ID instead of the `cancelled` flag in the timeout instance and `clearTimeout` that when `cancel` is called (`clearTimeout` doesn't complain when passed an already fired timeout ID). – FZs Oct 25 '21 at 21:08
  • Thanks @FZs! I didn't even know there was a `clearTimeout` function: I don't use `setTiemout` so often. That makes the code much more simple. – Marcello Del Buono Oct 25 '21 at 21:42
  • Thanks :), Close to how I had it at first. It does not do the *if done then do something else* - but can be done restructuring the action flow. In short the `do_something` first validates some data, retrieve and preprocess other data all accumulating in either return (abort action) or set timeout for action to finish after N ms. So perhaps something like `setAction(action, timeout, secondary_action)` where `secondary_action` is a potential second callback function. Would have to add secondary functions instead of doing promises. I'll look at it. There's usually some new angle that pops up heh. – Err488 Oct 26 '21 at 23:09