2

I have a ("micro-task"?) queue which tasks are promises. You can schedule them like this:

const task = schedule(queue, _ => {
    return /* valuable stuff */;
}).then(value => { /* do stuff with value */ });

now let's say that at some point I figure I don't need to do this task. I could just issue:

task.cancel()

I read about it being not possible to cancel promises in ES6 or something like that. But why can't I just help myself as follows?

function schedule(queue, fn) {
    let index;
    const promise = new Promise((resolve, reject) => {
        index = queue.push(() => {
            try {
                resolve(fn());
            } catch(error) {
                reject(error);
            }
        });
    });
    promise.cancel = () => {
        queue[index] = _ => {}; // just replace that callback with a no-op
    }
    return promise;
}

What is wrong with that? [apart from the fact that the index changes as the queue is worked and cannot be used to refer to the task, but that's a minor issue.]

Would it lead to some kind of memory leak because there is a Promise that remains pending forever? There is no longer a handle to the resolver function. Does the Promise hold on to the resolver function? And if so, does it use a weak reference? How do Promises keep themselves from being garbage collected if nobody is holding on to them, but they are simply expecting the then() or finally() to be called?

UPDATE Not sure this is really relevant, but Bergi asked the fair question that I haven't shows who works my queue. Let's just say, at some point something is working the queue.

In fact, I have made a Queue class from Array that has schedule and run:

class Queue extends Array {
    schedule(fn) {
        let index;
        const promise = new Promise((resolve, reject) => {
            index = this.push((...args) => {
                try {
                    resolve(fn(...args));
                } catch(error) {
                    reject(error);
                }
            });
        });
        promise.cancel = () => {
            this[index] = _ => {}; // just remove that callback
        }
        return promise;
    }
    run() {
        console.log("run", arguments);
        while(this.length > 0)
            this.shift()(...arguments);
    }
}

now I can do

const queue = new Queue()

and

const task = queue.schedule(_ => {
    return /* valuable stuff */;
}).then(value => { /* do stuff with value */ });

and then something might work the queue at some point.

function go() {
    if(queue.length > 0)
        setTimeout(function() {
            queue.run(...arguments);
        }
}

I was going to ask, what it even means to be a "micro-task" queue. But I didn't want to make this question become unfocused. I note that when a task adds another item to the queue, that gets executed in the same run. But when the action in the then() adds another item to the queue, then that is executed only in the next run.

Gunther Schadow
  • 1,490
  • 13
  • 22
  • 1
    Sure, you can do that. Promises don’t inherently inhibit cancellation or anything; they’re just not an abstraction for tasks in the first place. They also generally don’t cause memory leaks, and weak references don’t come into it; you can try implementing your own to figure out why that is. Leaving them unresolved is fine. Your queue index management looks like it’s going to run into trouble, though. Does the queue grow forever, or do indexes need to change? – Ry- Jul 15 '22 at 09:03
  • 1
    You could specify a `cancelFn` function as a 3rd argument to `schedule()` and have it call that function instead of `fn` when the promise is canceled. This way any cleanup that may be needed can be the responsibility of the function/object that schedules the function. – Derek Jul 15 '22 at 09:13
  • 1
    "*Does the Promise hold on to the resolver function?*" - no. The resolver function holds onto the promise that it can resolve, though. "*How do Promises keep themselves from being garbage collected if nobody is holding on to them?*" - they don't. In normal asynchronous usage, something is always holding onto the resolver function though until the promise is resolved. "*Would it lead to some kind of memory leak because there is a Promise that remains pending forever?*" - [no](https://stackoverflow.com/q/20068467/1048572). But you still shouldn't do it - rather reject the promise. – Bergi Jul 15 '22 at 09:13
  • 1
    "*I have a ("micro-task"?) queue*" - you haven't shown us the code that works the queue. It doesn't look like microtasks though. "*tasks are promises*" - tasks can at best be *functions* that return promises. Your functions however just *use* promises, they don't return them, so the "queue" can't even ensure they are processed sequentially. – Bergi Jul 15 '22 at 09:17
  • Perhaps my tiny [Asynchronous Queue (AQ)](https://github.com/kedicesur/AQ) package can help you. For example when you `.enqueue()` an async task to the queue an object is returned with an `.abort ()` method, used to cancel that particular task prematurely when invoked. In order to abort, it just forces that particular promise to reject and catches the rejection within itself. So in practice the promise gets cancelled silently to the outer world without being left to linger in the oblivion. – Redu Oct 26 '22 at 10:54

0 Answers0