Implementing an Asynchronous Limited Queue in JavaScript/TypeScript with async/await

Trying to grasp the concept of async/await, I am faced with the following code snippet:

class AsyncQueue<T> {
    queue = Array<T>()
    maxSize = 1

    async enqueue(x: T) {
        if (this.queue.length > this.maxSize) {
            // Block until available
        }

        this.queue.unshift(x)
    }

    async dequeue() {
        if (this.queue.length == 0) {
            // Block until available
        }

        return this.queue.pop()!
    }
}

async function produce<T>(q: AsyncQueue, x: T) {
    await q.enqueue(x)
}

async function consume<T>(q: AsyncQueue): T {
    return await q.dequeue()
}

// Expecting 3 4 in the console
(async () => {
    const q = new AsyncQueue<number>()
    consume(q).then(console.log)
    consume(q).then(console.log)
    produce(q, 3)
    produce(q, 4)
    consume(q).then(console.log)
    consume(q).then(console.log)
})()

The challenge lies within the "Block until available" sections of the code. Ideally, I want the execution to pause until a certain condition is met (for instance, dequeue waits until an enqueue occurs and vice versa based on available space). While considering the use of coroutines, I hope to confirm whether I am overlooking any hidden async/await magic.

Answer №1

Update on 17th April 2019: To cut the long story short, there appears to be a bug in the AsyncSemaphore implementation provided below, which was identified through property-based testing. If you're interested, you can read all about this "tale" here. Here is the corrected version:

class AsyncSemaphore {
    private promises = Array<() => void>()

    constructor(private permits: number) {}

    signal() {
        this.permits += 1
        if (this.promises.length > 0) this.promises.pop()!()
    }

    async wait() {
        this.permits -= 1
        if (this.permits < 0 || this.promises.length > 0)
            await new Promise(r => this.promises.unshift(r))
    }
}

After much effort and with inspiration from @Titian's answer, I believe I have resolved the issue. Although the code contains debug messages, it could be useful for educational purposes in understanding the flow of control:

class AsyncQueue<T> {
    waitingEnqueue = new Array<() => void>()
    waitingDequeue = new Array<() => void>()
    enqueuePointer = 0
    dequeuePointer = 0
    queue = Array<T>()
    maxSize = 1
    trace = 0

    async enqueue(x: T) {
        this.trace += 1
        const localTrace = this.trace

        if ((this.queue.length + 1) > this.maxSize || this.waitingDequeue.length > 0) {
            console.debug(`[${localTrace}] Producer Waiting`)
            this.dequeuePointer += 1
            await new Promise(r => this.waitingDequeue.unshift(r))
            this.waitingDequeue.pop()
            console.debug(`[${localTrace}] Producer Ready`)
        }

        this.queue.unshift(x)
        console.debug(`[${localTrace}] Enqueueing ${x} Queue is now [${this.queue.join(', ')}]`)

        if (this.enqueuePointer > 0) {
            console.debug(`[${localTrace}] Notify Consumer`)
            this.waitingEnqueue[this.enqueuePointer-1]()
            this.enqueuePointer -= 1
        }
    }

    async dequeue() {
        this.trace += 1
        const localTrace = this.trace

        console.debug(`[${localTrace}] Queue length before pop: ${this.queue.length}`)

        if (this.queue.length == 0 || this.waitingEnqueue.length > 0) {
            console.debug(`[${localTrace}] Consumer Waiting`)
            this.enqueuePointer += 1
            await new Promise(r => this.waitingEnqueue.unshift(r))
            this.waitingEnqueue.pop()
            console.debug(`[${localTrace}] Consumer Ready`)
        }

        const x = this.queue.pop()!
        console.debug(`[${localTrace}] Queue length after pop: ${this.queue.length} Popping ${x}`)

        if (this.dequeuePointer > 0) {
            console.debug(`[${localTrace}] Notify Producer`)
            this.waitingDequeue[this.dequeuePointer - 1]()
            this.dequeuePointer -= 1
        }

        return x
    }
}

Update: Here is a cleaner version utilizing an AsyncSemaphore, which encapsulates the common practice when working with concurrency primitives, adapted for asynchronous-CPS-single-threaded-event-loop™ style of JavaScript using async/await. With this updated logic in place, the functionality of AsyncQueue becomes more intuitive, and the dual synchronization through Promises is delegated to two separate semaphores:

class AsyncSemaphore {
    private promises = Array<() => void>()

    constructor(private permits: number) {}

    signal() {
        this.permits += 1
        if (this.promises.length > 0) this.promises.pop()()
    }

    async wait() {
        if (this.permits == 0 || this.promises.length > 0)
            await new Promise(r => this.promises.unshift(r))
        this.permits -= 1
    }
}

class AsyncQueue<T> {
    private queue = Array<T>()
    private waitingEnqueue: AsyncSemaphore
    private waitingDequeue: AsyncSemaphore

    constructor(readonly maxSize: number) {
        this.waitingEnqueue = new AsyncSemaphore(0)
        this.waitingDequeue = new AsyncSemaphore(maxSize)
    }

    async enqueue(x: T) {
        await this.waitingDequeue.wait()
        this.queue.unshift(x)
        this.waitingEnqueue.signal()
    }

    async dequeue() {
        await this.waitingEnqueue.wait()
        this.waitingDequeue.signal()
        return this.queue.pop()!
    }
}

Update 2: A subtle bug appeared hidden in the above code when attempting to use an AsyncQueue of size 0. The semantics do make sense: it is a queue without any buffer, where the publisher always awaits for a consumer to exist. The issue preventing it from functioning properly was located in these lines:

await this.waitingEnqueue.wait()
this.waitingDequeue.signal()

Upon closer inspection, one can see that dequeue() is not perfectly symmetrical to enqueue(). In fact, swapping the order of these two instructions:

this.waitingDequeue.signal()
await this.waitingEnqueue.wait()

Resolves the problem; intuitively, we should indicate that there is interest in dequeuing() before actually waiting for an enqueuing to occur.

I cannot guarantee this change won't reintroduce unforeseen bugs without comprehensive testing. Consider it a challenge ;)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Which five key JavaScript concepts should I grasp in order to excel as an AngularJS developer?

Coming from a background of server-side coding, I am delving into the world of AngularJS now. This means I need to have a solid grasp of JavaScript before diving in. If I don't have enough time to master JavaScript completely at the moment, what are ...

Encountered an issue when utilizing the useRef hook in Next.js version 13

I am currently exploring nextjs13 and typescript. I encountered an issue when attempting to use the useRef hook. Below is the code snippet in question: import { useEffect, useRef } from "react" export const useDraw = () => { const canvas ...

Creating a regular expression to match special characters using JavaScript

I'm making an attempt to verify certain characters in my code. If the input field contains specific characters for each character, it will return true; otherwise, it will return false. function isChar(value) { //Creating a regex pattern to permit ...

Is it possible to refresh the chat-box using PHP?

I recently created a chat box application using PHP, but I'm facing an issue with its automatic reload functionality. Is there a way to implement auto-reload in PHP itself, or would it be better to restructure the system to utilize AJAX? Additionally, ...

Create a hover effect on HTML map area using CSS

Is it possible to change the background color of an image map area on hover and click without using any third-party plugins? I attempted the following code: $(document).on('click', '.states', function(){ $(this).css("backgro ...

Callback function triggered upon the creation of a DOM node

I am in search of a way to have a specific callback function run each time a div that matches a particular selector is added to the DOM. I have explored the DOM events documentation and the event closest to what I need seems to be "load", however, it does ...

Could there be a mistake in the way array combinatorics are implemented in JavaScript?

Having encountered the necessity for generating unique combinations when dealing with multiple arrays, I developed this script. While it functions as intended during the combination process, storing the result in a final array yields unexpected outcomes. ...

Catalog of items in Mustache

I am currently working on generating HTML content using the mustache template engine. However, I have encountered an issue when trying to render a list of objects. Below is an example of my object: var Prod={ "Object1": { "name": "name1", "cat ...

Keep a vigilant eye on the peak utilization of memory within the Node.js process

I am in search of a way to effectively monitor the maximum memory usage in a Node.js process, regardless of whether there are memory leaks or not. The processes in question include both real applications and synthetic tests. My expectation is that it sho ...

Identify user input with Python Selenium for keyboard and mouse interactions

As a frequent user of Selenium Browser for my everyday browsing needs, I am looking to implement some code that will be triggered when certain keys are pressed on any page. Initially, I considered loading JavaScript onto each page to track key and mouse in ...

An unusual combination of '||' and '&&' occurred while utilizing babel

I've encountered an issue while building and publishing a package on npm with babel. The build process is showing me the following warning: Line 1: 'use strict' is unnecessary inside of modules strict Line 23: Unexpected mix of '| ...

Implementing Bootstrap 4 validation within a modal using JSON data

I've been struggling to validate the TextBoxFor before submitting the form. Despite trying various methods, I haven't managed to make it function properly. Modal: <div class="modal fade" id="MyModal_C"> <div class="modal-dialog" st ...

Parsing JSON sub items in Android application using Java

Here is a snippet of my PHP file: <?php $myObj = array( "name"=>"John" , "age"=>"30" , "post"=>[ "title"=>"What is WordPress" , "excerpt"=>"WordPress is a popular blogging platform" , ...

Mapping fields in Spring to JSON can be tricky, especially when dealing with fields that can be

In my object, there is a String value field which can contain either true or false. The issue arises when Spring/Jackson maps this to value: "true" as a String, causing problems with certain angularjs ng-model mappings that expect a boolean. Is there a way ...

Error during Next.js build: Incompatible types - cannot assign type to 'never'

Encountering an error in the console while attempting to build my project: .next/types/app/facebook/page.ts:8:13 Type error: Type 'OmitWithTag<typeof import("D:/projects/abkh24/client/app/facebook/page"), "metadata" | "defa ...

I am currently facing an issue where the code provided in the Puppeteer documentation is not functioning correctly due to an Un

I followed the code example on the Puppeteer documentation website but unfortunately, it's not working for me. I have Puppeteer 3.0.0 installed via npm. const puppeteer = require('puppeteer'); (async () => { const browser = await pup ...

Download multiple Highcharts graphs on a single page

When using Highchart Export, I am currently able to download multiple graphs in a single page PDF. However, I would like the first graph to be on the first page and the second graph on the second page when saving as a PDF. You can find the code in the fol ...

Methods to validate CSS attributes specified within a class using React testing library

I am struggling to validate the CSS properties defined within a class in CSS using the React Testing Library. Unfortunately, I am facing challenges in doing so. Here are the simplified code snippets: import React from "react"; import { render, ...

In Vue, it is not accurate to measure overflow

I am working on creating an overflow effect with tagging that fades out at the beginning to provide a subtle hint to users that there is more content. Here is what it currently looks like: https://i.stack.imgur.com/fXGBR.png To achieve this, I added a fa ...

Experiencing difficulties when uploading information to an API in a React JS application

Trying to Send Data via React to Contact Form API Having issues trying to send input data through the API when clicking submit button. The error encountered is that the API call should look like this Example Link However, it calls in a different way Diff ...