-1

I am looking to implement a job queue that ensures the response from an API is returned in the order of input items entered even in spite of each API call taking a variable amount of time potentially.

See codesandbox here https://codesandbox.io/s/sequential-api-response-eopue - When I input item such as 1, 12, 1234, 12345 in the input field and hit Enter, it goes to a simulated backend where I return item+-response to signify the output of corresponding input. However, I have used a different timeout on each call using Math.random() to simulate a real-world scenario where the API could take a non-deterministic amount of time.

Current output

processing:  1 
processing:  12 
processing:  123 
processing:  1234 
processing:  12345 
processing:  123456 
response: 1234-response 
response: 12-response 
response: 123456-response 
response: 123-response 
response: 1-response 
response: 12345-response 

Expected output The output I'd like to see is

processing:  1 
processing:  12 
processing:  123 
processing:  1234 
processing:  12345 
processing:  123456 
response: 1-response 
response: 12-response 
response: 123-response 
response: 1234-response 
response: 12345-response 
response: 123456-response 

My attempt: I have tried to implement the function getSequentialResponse (which is a wrapper over the function getNonSequentialResponse that generates the incorrect output above). This function adds the item that the user enters to a queue and does queue.shift() only when the lock variable _isBusy is released by getNonSequentialResponse indicating that the current promise has resolved and its ready to process the next. Until then, it waits in a while loop while the current item is being processed. My thinking was that since elements are always removed the head, the items will be processed in the order in which they were input.

Error: However, this, as I understood is the wrong approach since the UI thread is waiting and results in the error Potential infinite loop: exceeded 10001 iterations. You can disable this check by creating a sandbox.config.json file.

Dan O
  • 6,022
  • 2
  • 32
  • 50
LearnToImprove
  • 345
  • 5
  • 15

1 Answers1

2

A couple of things to consider here.

  1. The while loop is the wrong approach here - since we're working with asynchronous operations in JavaScript we need to keep in mind how the event loop works (here's a good talk if you need a primer). Your while loop will tie up the call stack and prevent the rest of the event loop (which includes the ES6 job queue, where Promises are dealt with, and the callback queue, where timeouts are dealt with) from occurring.
  2. So without a while loop, is there a way in JavaScript that we can control when to resolve a function so we can move onto the next one? Of course - it's Promises! We'll wrap the job in a Promise and only resolve that Promise when we're ready to move forward or reject it if there's an error.
  3. Since we're talking about a specific data structure, a queue, let's use some better terms to improve our mental model. We're not "processing" these jobs, we're "enqueuing" them. If we were processing them at the same time (i.e. "processing 1", "processing 2", etc.), we wouldn't be executing them sequentially.
export default class ItemProvider {
  private _queue: any;
  private _isBusy: boolean;

  constructor() {
    this._queue = [];
    this._isBusy = false;
  }

  public enqueue(job: any) {
    console.log("Enqueing", job);
    // we'll wrap the job in a promise and include the resolve 
    // and reject functions in the job we'll enqueue, so we can 
    // control when we resolve and execute them sequentially
    new Promise((resolve, reject) => {
      this._queue.push({ job, resolve, reject });
    });
    // we'll add a nextJob function and call it when we enqueue a new job;
    // we'll use _isBusy to make sure we're executing the next job sequentially
    this.nextJob();
  }

  private nextJob() {
    if (this._isBusy) return;
    const next = this._queue.shift();
    // if the array is empty shift() will return undefined
    if (next) {
      this._isBusy = true;
      next
        .job()
        .then((value: any) => {
          console.log(value);
          next.resolve(value);
          this._isBusy = false;
          this.nextJob();
        })
        .catch((error: any) => {
          console.error(error);
          next.reject(error);
          this._isBusy = false;
          this.nextJob();
        });
    }
  }
}

Now in our React code, we'll just make a fake async function using that helper function you made and enqueue the job!

import "./styles.css";
import ItemProvider from "./ItemProvider";
// import { useRef } from "react";

// I've modified your getNonSequentialResponse function as a helper 
// function to return a fake async job function that resolves to our item
const getFakeAsyncJob = (item: any) => {
  const timeout = Math.floor(Math.random() * 2000) + 500;
  // const timeout = 0;
  return () =>
    new Promise((resolve) => {
      setTimeout(() => {
        resolve(item + "-response");
      }, timeout);
    });
};

export default function App() {
  const itemProvider = new ItemProvider();

  function keyDownEventHandler(ev: KeyboardEvent) {
    if (ev.keyCode === 13) {
      const textFieldValue = (document.getElementById("textfieldid") as any)
        .value;

      // not sequential
      // itemProvider.getNonSequentialResponse(textFieldValue).then((response) => {
      //   console.log("response: " + response);
      // });
      
      // we make a fake async function tht resolves to our textFieldValue
      const myFakeAsyncJob = getFakeAsyncJob(textFieldValue);
      // and enqueue it 
      itemProvider.enqueue(myFakeAsyncJob);
    }
  }

  return (
    <div className="App">
      <input
        id="textfieldid"
        placeholder={"Type and hit Enter"}
        onKeyDown={keyDownEventHandler}
        type="text"
      />

      <div className="displaylabelandbox">
        <label>Display box below</label>
        <div className="displaybox">hello</div>
      </div>
    </div>
  );
}

Here's the codesandbox.

tiagomagalhaes
  • 617
  • 1
  • 8
  • 15
Brendan Bond
  • 1,737
  • 1
  • 10
  • 8
  • thank you! I read your implementation a few times and played with it. I love the concept ot the fake async job which is a function that returns the promise and understand how it's guarded by the _isBusy flag. However, I don't understand why enque needs to have a promise. Why does the queue need to be inside of a promise? I modified the enqueue slightly to remove the promise here, and still get the same output, unless I am mistaken. https://codesandbox.io/s/sequential-api-response-forked-6b9ej?file=/src/ItemProvider.ts Would you explain to me why the Promise is needed in enqueue func? – LearnToImprove Aug 04 '21 at 03:28
  • 1
    A couple of reasons - what if you couldn't guarantee that the job was asynchronous? (I realize we'd need to refactor the nextJob() function to either test for a Promise or use async/await syntax in this case). Wrapping a Promise assures that the queue will run like we want it to. Also, what if for some reason a user of this API wanted the value returned by the job? In your implementation you've easily called console.log() on the value, but nextJob() is a private method to the ItemProvider class, how would a user of this API get at that value? We resolve() it up the chain – Brendan Bond Aug 04 '21 at 14:30