Search code examples
javascriptnode.jspromisedeferred

Can you write this without using a Deferred?


I wrote some code below that uses promises and the easiest way I could find to write it was using a Deferred object instead of the usual Promise executor function because I need to resolve the promise from outside the executor. I'm wondering if there's an accepted design pattern based on the Promise executor function for a problem like this that doesn't use a deferred-like solution? Can it be done without having to resolve the promise from outside the promise executor?

Here are the details.

I have a project that uses a set of Worker Threads and various parts of the code that want to use a Worker Thread from time to time. To manage that, I've created a simple WorkerList class that keeps a list of the available Worker Threads. When someone wants to use one, they call get() on it and that returns a promise that resolves to a Worker Thread. If a worker thread is available immediately, the promise resolves immediately. If all worker threads are in use (and thus the list of available workers is empty), then the promise doesn't resolve until one is later put back into the available list via the add(worker) method.

This WorkerList class has only two methods, add(worker) and get(). You get() a worker and when you're done with it, you add(worker) it back. When you add(worker) it back, the class checks to see if there are any tasks waiting for an available Worker. If there, are, it resolves their promise with an available Worker. That resolving of someone else's promise is where the Deferred was used.

Here's the code for the WorkerList:

class WorkerList {
    constructor() {
        this.workers = [];
        this.deferredQueue = [];
    }
    add(worker) {
        this.workers.push(worker);

        // if someone is waiting for a worker,
        // pull the oldest worker out of the list and
        // give it to the oldest deferred that is waiting
        while (this.deferredQueue.length && this.workers.length) {
            let d = this.deferredQueue.shift();
            d.resolve(this.workers.shift());
        }
    }
    // if there's a worker, get one immediately
    // if not, return a promise that resolves with a worker
    //    when next one is available
    get() {
        if (this.workers.length) {
            return Promise.resolve(this.workers.shift());
        } else {
            let d = new Deferred();
            this.deferredQueue.push(d);
            return d.promise;
        }
    }
}

And, here's the Deferred implementation:

function Deferred() {
    if (!(this instanceof Deferred)) {
        return new Deferred();
    }
    const p = this.promise = new Promise((resolve, reject) => {
        this.resolve = resolve;
        this.reject = reject;
    });
    this.then = p.then.bind(p);
    this.catch = p.catch.bind(p);
    if (p.finally) {
        this.finally = p.finally.bind(p);
    }
}

Solution

  • Maybe the below is just a poor man's approach to deferreds, and doesn't really get to the crux of the matter, but instead of a queue of deferreds, you could just keep a queue of resolver functions.

    This saves a small amount of code over your approach and avoids explicitly using Deferreds.

    I don't know if there is an established pattern for this, but this in itself seems like a reusable pattern for maintaining an asynchronous pool of objects, so rather than calling it WorkerList, you could name it AsyncPool, and then compose that as a reusable piece within your WorkerList:

    class AsyncPool {
        constructor() {
            this.entries = [];
            this.resolverQueue = [];
        }
        add(entry) {
            console.log(`adding ${entry}`);
            this.entries.push(entry);
    
            // if someone is waiting for an entry,
            // pull the oldest one out of the list and
            // give it to the oldest resolver that is waiting
            while (this.resolverQueue.length && this.entries .length) {
                let r = this.resolverQueue.shift();
                r(this.entries.shift());
            }
        }
        // if there's an entry, get one immediately
        // if not, return a promise that resolves with an entry
        //    when next one is available
        get() {
            return new Promise((r) => 
                this.entries.length
                    ? r(this.entries.shift())
                    : this.resolverQueue.push(r)
            );
        }
    }
    
    
    let pool = new AsyncPool();
    
    pool.add('Doc');
    pool.add('Grumpy');
    pool.get().then(console.log);
    pool.get().then(console.log);
    pool.get().then(console.log);
    pool.get().then(console.log);
    
    // add more entries later
    setTimeout(() => pool.add('Sneezy'), 1000);
    setTimeout(() => pool.add('Sleepy'), 2000);