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.
A couple of things to consider here.
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>
);
}