queues.js

import {Future} from './futures.js';
import * as locks from './locks.js';


/**
 * Indicates that the queue is empty.
 *
 * @extends external:Error
 */
export class QueueEmpty extends Error {}

/**
 * Indicates that the queue is full.
 *
 * @extends external:Error
 */
export class QueueFull extends Error {}

/**
 * @typedef QueueWaitOptions
 * @type {Object}
 * @property {Number} [size] - Wait until the available items meets or exceeds this value.
 */

/**
 * A classic producer/consumer construct for regulating work.
 *
 * @see Python's [asyncio.Queue]{@link https://docs.python.org/3/library/asyncio-queue.html}
 * @param {Number} [maxsize=0] - The number of items allowed to be stored before blocking.
 */
export class Queue {
    constructor(maxsize=0) {
        this._maxsize = maxsize;
        this._getters = [];
        this._putters = [];
        this._unfinishedTasks = 0;
        this._finished = new locks.Event();
        this._finished.set();
        this._queue = [];
    }

    /**
     * @protected
     */
    _get() {
        return this._queue.shift();
    }

    /**
     * @protected
     */
    _put(item) {
        return this._queue.push(item);
    }

    _wakeupNext(waiters) {
        while (waiters.length) {
            const w = waiters.shift();
            if (!w.done()) {
                w.setResult();
                break;
            }
        }
    }

    /**
     * The number of items waiting to be dequeued.
     * @type {Number}
     */
    get size() {
        return this._queue.length;
    }

    /**
     * The maximum number of items that can be enqueued.
     * @type {Number}
     */
    get maxsize() {
        return this._maxsize;
    }

    /**
     * {@link true} if a call to [put]{@link Queue#put} would block.
     * @type {boolean}
     */
    get full() {
        if (this._maxsize <= 0) {
            return false;
        } else {
            return this._queue.length >= this._maxsize;
        }
    }

    /**
     * Place a new item in the queue if it is not full.  Otherwise block until space is
     * available.
     *
     * @param {*} item - Any object to pass to the caller of [dequeue]{@link Queue#dequeue}.
     */
    async put(item) {
        while (this.full) {
            const putter = new Future();
            this._putters.push(putter);
            try {
                await putter;
            } catch(e) {
                if (!this.full) {
                    this._wakeupNext(this._putters);
                }
                throw e;
            }
        }
        return this.putNoWait(item);
    }

    /**
     * Place a new item in the queue if it is not full.
     *
     * @param {*} item - Any object to pass to the caller of [dequeue]{@link Queue#dequeue}.
     * @throws {QueueFull}
     */
    putNoWait(item) {
        if (this.full) {
            throw new QueueFull();
        }
        this._put.apply(this, arguments);
        this._unfinishedTasks++;
        this._finished.clear();
        this._wakeupNext(this._getters);
    }

    /**
     * Wait for an item to be available.
     *
     * @param {QueueWaitOptions} [options]
     */
    async wait(options={}, _callback) {
        const size = options.size == null ? 1 : options.size;
        while (this.size < size) {
            const getter = new Future();
            this._getters.push(getter);
            try {
                await getter;
            } catch(e) {
                if (this.size) {
                    this._wakeupNext(this._getters);
                }
                throw e;
            }
        }
        if (_callback) {
            return _callback();
        }
    }

    /**
     * Get an item from the queue if it is not empty.  Otherwise block until an item is available.
     *
     * @param {QueueWaitOptions} [options]
     * @returns {*} An item from the head of the queue.
     */
    async get(options) {
        return await this.wait(options, () => this.getNoWait());
    }

    /**
     * Get an item from the queue if it is not empty.
     *
     * @throws {QueueEmpty}
     * @returns {*} An item from the head of the queue.
     */
    getNoWait() {
        if (!this.size) {
            throw new QueueEmpty();
        }
        const item = this._get();
        this._wakeupNext(this._putters);
        return item;
    }

    /**
     * Get all items from the queue.
     *
     * @param {QueueWaitOptions} [options]
     * @returns {Array} An array of items from the queue.
     */
    async getAll(options) {
        return await this.wait(options, () => this.getAllNoWait());
    }

    /**
     * Get all items from the queue without waiting.
     */
    getAllNoWait() {
        const items = [];
        while (this.size) {
            items.push(this._get());
        }
        this._wakeupNext(this._putters);
        return items;
    }

    /**
     * Decrement the number of pending tasks.  Called by consumers after completing
     * their use of a dequeued item to indicate that processing has finished.
     *
     * When all dequeued items have been accounted for with an accompanying call to
     * this function [join]{@link Queue#join} will unblock.
     *
     * @param {Number} [count=1] - The number of tasks to mark as done.
     */
    taskDone(count=1) {
        if (this._unfinishedTasks - count < 0) {
            throw new Error('Called too many times');
        }
        this._unfinishedTasks -= count;
        if (this._unfinishedTasks === 0) {
            this._finished.set();
        }
    }

    /**
     * Will block until all items are dequeued and for every item that was dequeued a call
     * was made to [taskdone]{@link Queue#taskDone}.
     */
    async join() {
        if (this._unfinishedTasks > 0) {
            await this._finished.wait();
        }
    }
}


/**
 * A subtype of {@link Queue} that lets the producer control the ordering of pending items
 * with a priority argument.
 *
 * @see Python's [asyncio.PriorityQueue]{@link https://docs.python.org/3/library/asyncio-queue.html#priority-queue}
 * @extends Queue
 */
export class PriorityQueue extends Queue {
    _put(item, prio) {
        this._queue.push([prio, item]);
        this._queue.sort((a, b) => b[0] - a[0]);
    }

    _get() {
        return this._queue.pop()[1];
    }

    /**
     * Place a new item in the queue if it is not full.  Otherwise block until space is
     * available.
     *
     * @param {*} item - Any object to pass to the caller of [dequeue]{@link Queue#dequeue}.
     * @param {Number} prio - The sort order for this item.
     */
    async put(item, prio) {
        return await super.put(item, prio);
    }
}


/**
 * A Last-In-First-Out Queue.  Items are dequeued in the opposite order that
 * they are enqueued.
 *
 * @see Python's [asyncio.LifoQueue]{@link https://docs.python.org/3/library/asyncio-queue.html#lifo-queue}
 * @extends Queue
 */
export class LifoQueue extends Queue {
    _get() {
        return this._queue.pop();
    }
}


/**
 * The built in Error object.
 *
 * @external Error
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error}
 */