How to sequence a series of dynamically generated asynch operations using promises? - javascript

In the following example, I am simulating a scenario where an user can trigger some actions, every action is a asynchronous operation (wrapped in a promise) which can be resolved with a random timing.
The list of actions is also dynamic, an user can trigger only one actions or many in a span of time.
I would need:
Keep the promise resolution sequential, keeping in consideration that a full list of actions is not known in advance.
I can use ES6 and Browser native Promises
To test the example, click several times (with variable frequency) the button.
(function (window) {
document.addEventListener('DOMContentLoaded', e => {
let logActionsElm = document.getElementById('logActions');
let logOperationsElm = document.getElementById('logOperations');
let logCounterElm = document.getElementById('logCounter');
let btnActionElm = document.getElementById('btnAction');
let actionCounter = 0;
let operationDurations = [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000];
let getRandomNumber = (min, max) => Math.floor(Math.random() * (max - 0 + min)) + min;
let getRandomOperationDuration = x => operationDurations[getRandomNumber(0, 8)];
let promises = [];
let createAction = (id, duration) => {
logActionsElm.innerHTML += `${id} action_start_duration: ${duration} --------- ${id}<br>`;
let promiseAction = new Promise((resolve, reject) => {
setTimeout(e => {
logActionsElm.innerHTML += `${id} action_end___duration: ${duration} --------- ${id}<br>`;
}, duration);
});
};
let createOperation = x => {
actionCounter++;
let duration = getRandomOperationDuration() / 10;
createAction(actionCounter, duration);
//logActionsElm.innerHTML += `action ${actionCounter} created will resolve after ${duration}<br>`;
};
btnActionElm.addEventListener('click', e => {
createOperation();
});
var counter = 0;
setInterval(x => {
if (counter >= 20) {
return;
}
counter++;
logCounterElm.innerHTML += `${counter} second <br>`;
}, 1000);
});
})(window);
body {
font-size: 1.5em;
font-family: 'Courier New';
}
#logCounter {
position: fixed;
top: 0;
right: 0;
margin: 2em;
}
<button id="btnAction">Triger and action</button>
<div id="logActions"></div>
<div id="logOperations"></div>
<div id="logCounter"></div>

You could append new action functions to a single promise to ensure actions aren't evaluated until after all previous actions have resolved:
let queue = Promise.resolve();
let userActionIdCounter = 0;
function queueAction(fn) {
queue = queue.then(fn);
return queue;
}
function userAction() {
return new Promise((resolve) => {
const seconds = Math.ceil(Math.random() * 5);
const actionId = userActionIdCounter;
userActionIdCounter += 1;
console.log('User action', userActionIdCounter, 'started');
setTimeout(() => {
console.log('User action', userActionIdCounter, 'complete');
resolve();
}, seconds * 1000);
});
}
document.getElementById('action').addEventListener('click', () => {
queueAction(userAction);
});
<button id='action'>Trigger action</button>

You should NOT create your own solution. Instead, select from one of the approximately 158 existing solutions here https://www.npmjs.com/search?q=promise+queue

Related

How do I get UI to update in loop using forEach

Promise.all([
seperatingDMCM(),
compileDirectMessage(),
renderingResult(),
addResultButtonListener(),
]);
I have a progress bar in my UI and I have 4 functions mentioned above each of which returns a Promise. I have a loop inside the first function seperatingDMCM(), that will handle all the my data. I want to increment the progress bar with each increment of the loop. Meaning each loop iteration should be async, so the UI will update and only afterwards the loop will iterate. When the loop has ended I want to return a promise so that the other functions will begin to execute. I am facing an issue that progress bar is not working as it suppose to and is immediately being invoked when seperatingDMCM() returns the promise () and not asynchronously. This progress bar is immediately being updated to 100% on the next page instead of live updates with small increment on the current page.
This is my updateUI function:
function startProgressBar(i, arr) {
return new Promise((resolve) => {
setTimeout(() => {
i = (i * 100) / arr.length;
console.log(i);
let elem = document.getElementById("progressBar");
if (i < 100) {
elem.innerText = i + "%";
elem.style.width = i + "%";
elem.innerHTML = i + "%";
resolve();
}
}, 0);
});
}
This is my first function where I want to update the UI per loop iteration
function seperatingDMCM() {
const contentType = "Content type";
return new Promise((resolve) => {
rowObject.forEach(async (row, index) => {
const creatingInts = () => {
console.log("CreatedInt at", index);
row["Date created (UTC)"] = ExcelDateToJSDate(
row["Date created (UTC)"]
);
if (
row[contentType] !== "DM" &&
row.hasOwnProperty("Falcon user name")
) {
publicCommentsCount++;
interaction = { row : row};
compiledInteractions.push(interaction);
interaction = {};
} else {
dmData.push(row);
}
};
startProgressBar(index, rowObject).then(creatingInts());
});
quickSort(dmData, "Falcon URL");
console.log("SORTED", dmData);
console.log(workbook);
console.log("Rows", rowObject);
resolve();
});
}

Calling asynchronous socket.io io.emit() in synchronous way

Completely new to socket io here.
I want to emit data running from within a loop that I am running on my server. The loop runs for a while (a few seconds, up to a few minutes) and I want to emit data from my node server, that I then want to visualize on the client side in the browser (with chart js). Basically real time data charting.
The problem that occured is that apparently the emits are only fired after the loop has finished. From my understanding that is, because the emit function is asynchronous (non-blocking). And since the loop itself blocks until it is finished (originally used a while loop), the asynchronous calls are only fired afterwards. Correct?
while (CONDITION) {
...
io.emit('data update', DATA);
}
Now I have come up with my own solution, but it feels clonky. And I am wondering if there are better, more by the book ways to do this. What I am doing now is calling the loop step (where in the end I call emit) recursively as the callback to a setTimeout() with duration set to 0.
function loopStep() {
if (CONDITION) {
...
io.emit('data update', DATA);
setTimeout(loopStep,0)
}
}
setTimeout(loopStep,0);
My question is, are there other, better ways?
EDIT:
I was asked to add the full code.
Here is the full code code with the earlier (loop) version (as far as I can remember it, had to quickly reconstruct it here):
let stp = 0;
while (stp < maxLogStep) {
let new_av_reward;
var smallStep = 0;
while (smallStep < maxSmallStep) {
let ran1 = get_random();
let cum_prob = 0;
let chosen_action = pref.findIndex((el, i) => {
let pro = prob(i);
let result = pro + cum_prob > ran1;
cum_prob += pro;
return result;
})
let cur_reward = get_reward(chosen_action);
if (stp*maxSmallStep + smallStep == 0) {
new_av_reward = cur_reward
} else {
new_av_reward = prev_av_reward + (cur_reward - prev_av_reward) / (stp * maxSmallStep + smallStep)
}
let cur_prob = prob(chosen_action);
pref.forEach((element, index, array) => {
if (chosen_action === index) {
array[index] = element + stepSize * (cur_reward - new_av_reward) * (1 - cur_prob)
} else {
array[index] = element - stepSize * (cur_reward - new_av_reward) * (cur_prob)
}
});
prev_av_reward = new_av_reward;
smallStep++
}
io.emit('graph update', {
learnedProb: [prob(0), prob(1), prob(2)],
averageReward: new_av_reward,
step: stp * maxSmallStep + smallStep
});
stp++
};
And here is the recursive function with setTimeout:
function learnStep(stp, prev_av_reward) {
let new_av_reward;
if (stp < maxLogStep) {
var smallStep = 0;
while (smallStep < maxSmallStep) {
let ran1 = get_random();
let cum_prob = 0;
let chosen_action = pref.findIndex((el, i) => {
let pro = prob(i);
let result = pro + cum_prob > ran1;
cum_prob += pro;
return result;
})
let cur_reward = get_reward(chosen_action);
if (stp*maxSmallStep + smallStep == 0) {
new_av_reward = cur_reward
} else {
new_av_reward = prev_av_reward + (cur_reward - prev_av_reward) / (stp * maxSmallStep + smallStep)
}
let cur_prob = prob(chosen_action);
pref.forEach((element, index, array) => {
if (chosen_action === index) {
array[index] = element + stepSize * (cur_reward - new_av_reward) * (1 - cur_prob)
} else {
array[index] = element - stepSize * (cur_reward - new_av_reward) * (cur_prob)
}
});
prev_av_reward = new_av_reward;
smallStep++
}
io.emit('graph update', {
learnedProb: [prob(0), prob(1), prob(2)],
averageReward: new_av_reward,
step: stp * maxSmallStep + smallStep
});
setTimeout(learnStep, 0, stp + 1, new_av_reward);
};
If your goal is to make sure you hold up your while loop until the emit completes and the client receives and acknowledges that data, you may want to consider using Promises and socket.io acknowledgement feature like the below example:
Server Side
io.on("connection", async (socket) => {
// ...
while (CONDITION) {
// ...
await new Promise((resolve, reject) => {
io.emit('data update', data, (ack) => {
//client acknowledged received
resolve(true)
});
});
}
});
Client Side
socket.on("data update", (data, fn) => {
console.log(data)
fn("ack");
});

How to make the onmessage phase of a webworker asynchronous?

I'm using a webworker to calculate coordinates and values belonging to those places. The calculations happen in the background perfectly, keeping the DOM responsive. However, when I send the data from the webworker back to the main thread the DOM becomes unresponsive for a part of the transfer time.
My webworker (sending part):
//calculates happen before; this is the final step to give the calculated data back to the mainthread.
var processProgressGEO = {'cmd':'geoReport', 'name': 'starting transfer to main', 'current': c, 'total': polys}
postMessage(processProgressGEO);
postMessage({
'cmd':'heatmapCompleted',
'heatdata': rehashedMap,
'heatdatacount': p,
'current': c,
'total': polys,
'heatmapPeak': peakHM,
});
self.close();
The variable rehashedMap in the code snippet above is an object with numerical keys. Each key contains an array with another object in.
My mainthread (only the relevant part:)
var heatMaxforAuto = 1000000; //maximum amount of datapoints allowed in the texdata. This takes into account the spread of a singel datapoint.
async function fetchHeatData(){
return new Promise((resolve, reject) => {
var numbercruncher = new Worker('calculator.js');
console.log("Performing Second XHR request:");
var url2 = 'backend.php?datarequest=geodata'
$.ajax({
type: "GET",
url: url2,
}).then(async function(RAWGEOdata) {
data.georaw = RAWGEOdata;
numbercruncher.onmessage = async function(e){
var w = (e.data.current/e.data.total)*100+'%';
if (e.data.cmd === 'geoReport'){
console.log("HEAT: ", e.data.name, end(),'Sec.' );
}else if (e.data.cmd === 'heatmapCompleted') {
console.log("received Full heatmap data: "+end());
data.heatmap = e.data.heatdata;
console.log("heatData transfered", end());
data.heatmapMaxValue = e.data.heatmapPeak;
data.pointsInHeatmap = e.data.heatdatacount;
console.log("killing worker");
numbercruncher.terminate();
resolve(1);
}else{
throw "Unexpected command received by worker: "+ e.data.cmd;
}
}
console.log('send to worker')
numbercruncher.postMessage({'mode':'geo', 'data':data});
}).catch(function(error) {
reject(0);
throw error;
})
});
}
async function makemap(){
let heatDone = false;
if (data.texdatapoints<= heatMaxforAuto){
heatDone = await fetchHeatData();
}else{
var manualHeatMapFetcher = document.createElement("BUTTON");
var manualHeatMapFetcherText = document.createTextNode('Fetch records');
manualHeatMapFetcher.appendChild(manualHeatMapFetcherText);
manualHeatMapFetcher.id='manualHeatTriggerButton';
manualHeatMapFetcher.addEventListener("click", async function(){
$(this).toggleClass('hidden');
heatDone = await fetchHeatData();
console.log(heatDone, 'allIsDone', end());
});
document.getElementById("toggleIDheatmap").appendChild(manualHeatMapFetcher);
}
}
makemap();
The call to the end() function is needed to calculate the seconds since the start of the webworker. It returns the difference between a global set starttime and the time of calling.
What shows in my console:
HEAT: starting transfer to main 35 Sec. animator.js:44:19
received Full heatmap data: 51 animator.js:47:19
heatData transfered 51 animator.js:49:19
killing worker animator.js:52:19
1 allIsDone 51
The issue:
My DOM freezes between the start of the data transfer and the message after receiving the full heatmap data. This is the phase between the first and second message in my console. It takes 16 seconds to transfer, but the DOM only goes unresponsive once for a part of that time. Webworkers can't share data with the mainthread, so a transfer is needed.
Question:
Firstly, how to prevent the freeze of the the DOM during the onmessage phase of the webworker? Secondly, more out of curiosity: how can this freeze only occur during a part of that phase, as these are triggered by two consecutive steps with nothing going on in between?
What I tried so far:
Doing a for-loop over the rehashedMap and return key by key. This still triggers DOM freezes; shorter, but more than once. In rare occurrences it takes the tab down.
Looking for a way to buffer the onmessage phase; however, there's no such option specified in the documentation (https://developer.mozilla.org/en-US/docs/Web/API/Worker/onmessage) as compared to the postMessage phase (https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage). Am I missing something here?
As a test I replaced the rehashedMap with an empty object; this didn't cause any freezes in the DOM. Of course, this is leaves me without access to the calculate data.
I looked at this thread on SO:Javascript WebWorker - Async/Await But I'm not sure how to compare that context to mine.
Options
It's understandable you should associate this with the web worker, but it probably doesn't have anything to do with it. I was wrong, it does. I see two possible reasons for the problem:
(We know this is not true for the OP, but may still be relevant for others.) The problem is probably that you have a lot of DOM manipulation to do once you've received the heat map. If you do that in a tight loop that never lets the main thread do anything else, the page will be unresponsive during that time.
If that's what's going on, you have to either find a way to do the DOM manipulation more quickly (sometimes that's possible, other times not) or find a way to carve it up into chunks and process each chunk separately, yielding back to the browser between chunks so that the browser can handle any pending UI work (including rendering the new elements).
You haven't included the DOM work being done with the heat map so it's not really possible to give you code to solve the problem, but the "carving up" would be done by processing a subset of the data and then using setTimeout(fn, 0) (possibly combined with requestAnimationFrame to ensure that a repaint has occurred) to schedule continuing the work (using fn) after briefly yielding to the browser.
If it's really the time spent transferring the data between the worker and the main thread, you might be able to use a transferable object for your heat map data rather than your current object, although doing so may require significantly changing your data structure. With a transferable object, you avoid copying the data from the worker to the main thread; instead, the worker transfers the actual memory to the main thread (the worker loses access to the transferable object, and the main thread gains access to it — all without copying it). For instance, the ArrayBuffer used by typed arrays (Int32Array, etc.) is transferable.
If it's really the time spent receiving the data from the worker (and from your experiments it sounds like it is), and using a transferable isn't an option (for instance, because you need the data to be in a format that isn't compatible with a transferable), the only remaining option I can see is to have the worker send the main script smaller blocks of data spaced out enough for the main thread to remain responsive. (Perhaps even sending the data as it becomes available.)
Closer look at #3
You've described an array of 1,600 entries, where each entry is an array with between 0 and "well over 7,000" objects, each with three properties (with number values). That's over 5.6 million objects. It's no surprise that cloning that data takes a fair bit of time.
Here's an example of the problem you've described:
const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
worker.addEventListener("message", ({data}) => {
if ((data && data.action) === "data") {
console.log(Date.now(), `Received ${data.array.length} rows`);
if (data.done) {
stopSpinning();
}
}
});
document.getElementById("btn-go").addEventListener("click", () => {
console.log(Date.now(), "requesting data");
startSpinning();
worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
if (spinner) {
cancelAnimationFrame(spinHandle);
maxDelay = 0;
queueUpdate();
}
}
function queueUpdate() {
intervalStart = Date.now();
spinHandle = requestAnimationFrame(() => {
updateMax();
spinner.textContent = states[stateIndex];
stateIndex = (stateIndex + 1) % states.length;
if (spinHandle) {
queueUpdate();
}
});
}
function stopSpinning() {
updateMax();
cancelAnimationFrame(spinHandle);
spinHandle = 0;
if (spinner) {
spinner.textContent = "Done";
console.log(`Max delay between frames: ${maxDelay}ms`);
}
}
function updateMax() {
if (intervalStart !== 0) {
const elapsed = Date.now() - intervalStart;
if (elapsed > maxDelay) {
maxDelay = elapsed;
}
}
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
if ((data && data.action) === "go") {
console.log(Date.now(), "building data");
const array = Array.from({length: 1600}, () =>
Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
);
console.log(Date.now(), "data built");
console.log(Date.now(), "sending data");
postMessage({
action: "data",
array,
done: true
});
console.log(Date.now(), "data sent");
}
});
</script>
Here's an example of the worker sending the data in chunks as fast as it can but in separate messages. It makes the page responsive (though still jittery) when receiving the data:
const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
if ((data && data.action) === "data") {
if (clockTimeStart === 0) {
clockTimeStart = Date.now();
console.log(Date.now(), "Receiving data");
}
array.push(...data.array);
if (data.done) {
console.log(Date.now(), `Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
stopSpinning();
}
}
});
document.getElementById("btn-go").addEventListener("click", () => {
console.log(Date.now(), "requesting data");
array = [];
clockTimeStart = 0;
startSpinning();
worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
if (spinner) {
cancelAnimationFrame(spinHandle);
maxDelay = 0;
queueUpdate();
}
}
function queueUpdate() {
intervalStart = Date.now();
spinHandle = requestAnimationFrame(() => {
updateMax();
spinner.textContent = states[stateIndex];
stateIndex = (stateIndex + 1) % states.length;
if (spinHandle) {
queueUpdate();
}
});
}
function stopSpinning() {
updateMax();
cancelAnimationFrame(spinHandle);
spinHandle = 0;
if (spinner) {
spinner.textContent = "Done";
console.log(`Max delay between frames: ${maxDelay}ms`);
}
}
function updateMax() {
if (intervalStart !== 0) {
const elapsed = Date.now() - intervalStart;
if (elapsed > maxDelay) {
maxDelay = elapsed;
}
}
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
if ((data && data.action) === "go") {
console.log(Date.now(), "building data");
const array = Array.from({length: 1600}, () =>
Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
);
console.log(Date.now(), "data built");
const total = 1600;
const chunks = 100;
const perChunk = total / chunks;
if (perChunk !== Math.floor(perChunk)) {
throw new Error(`total = ${total}, chunks = ${chunks}, total / chunks has remainder`);
}
for (let n = 0; n < chunks; ++n) {
postMessage({
action: "data",
array: array.slice(n * perChunk, (n + 1) * perChunk),
done: n === chunks - 1
});
}
}
});
</script>
Naturally it's a tradeoff. The total clock time spent receiving the data is longer the smaller the chunks; the smaller the chunks, the less jittery the page is. Here's really small chunks (sending each of the 1,600 arrays separately):
const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
if ((data && data.action) === "data") {
if (clockTimeStart === 0) {
clockTimeStart = Date.now();
}
array.push(data.array);
if (data.done) {
console.log(`Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
stopSpinning();
}
}
});
document.getElementById("btn-go").addEventListener("click", () => {
console.log(Date.now(), "requesting data");
array = [];
clockTimeStart = 0;
startSpinning();
worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
if (spinner) {
cancelAnimationFrame(spinHandle);
maxDelay = 0;
queueUpdate();
}
}
function queueUpdate() {
intervalStart = Date.now();
spinHandle = requestAnimationFrame(() => {
updateMax();
spinner.textContent = states[stateIndex];
stateIndex = (stateIndex + 1) % states.length;
if (spinHandle) {
queueUpdate();
}
});
}
function stopSpinning() {
updateMax();
cancelAnimationFrame(spinHandle);
spinHandle = 0;
if (spinner) {
spinner.textContent = "Done";
console.log(`Max delay between frames: ${maxDelay}ms`);
}
}
function updateMax() {
if (intervalStart !== 0) {
const elapsed = Date.now() - intervalStart;
if (elapsed > maxDelay) {
maxDelay = elapsed;
}
}
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
if ((data && data.action) === "go") {
console.log(Date.now(), "building data");
const array = Array.from({length: 1600}, () =>
Array.from({length: Math.floor(r() * 7000)}, () => ({lat: r(), lng: r(), value: r()}))
);
console.log(Date.now(), "data built");
array.forEach((chunk, index) => {
postMessage({
action: "data",
array: chunk,
done: index === array.length - 1
});
});
}
});
</script>
That's building all the data and then sending it, but if building the data times time, interspersing building and sending it may make the page responsiveness smoother, particularly if you can send the inner arrays in smaller pieces (as even sending ~7,000 objects still causes jitter, as we can see in the last example above).
Combining #2 and #3
Each entry in your main array is an array of objects with three numeric properties. We could instead send Float64Arrays with those values in lat/lng/value order, using the fact they're transferable:
const workerCode = document.getElementById("worker").textContent;
const workerBlob = new Blob([workerCode], { type: "text/javascript" });
const workerUrl = (window.webkitURL || window.URL).createObjectURL(workerBlob);
const worker = new Worker(workerUrl);
let array = null;
let clockTimeStart = 0;
worker.addEventListener("message", ({data}) => {
if ((data && data.action) === "data") {
if (clockTimeStart === 0) {
clockTimeStart = Date.now();
}
const nums = data.array;
let n = 0;
const entry = [];
while (n < nums.length) {
entry.push({
lat: nums[n++],
lng: nums[n++],
value: nums[n++]
});
}
array.push(entry);
if (data.done) {
console.log(Date.now(), `Received ${array.length} row(s) in total, clock time to receive data: ${Date.now() - clockTimeStart}ms`);
stopSpinning();
}
}
});
document.getElementById("btn-go").addEventListener("click", () => {
console.log(Date.now(), "requesting data");
array = [];
clockTimeStart = 0;
startSpinning();
worker.postMessage({action: "go"});
});
const spinner = document.getElementById("spinner");
const states = [..."▁▂▃▄▅▆▇█▇▆▅▄▃▂▁"];
let stateIndex = 0;
let spinHandle = 0;
let maxDelay = 0;
let intervalStart = 0;
function startSpinning() {
if (spinner) {
cancelAnimationFrame(spinHandle);
maxDelay = 0;
queueUpdate();
}
}
function queueUpdate() {
intervalStart = Date.now();
spinHandle = requestAnimationFrame(() => {
updateMax();
spinner.textContent = states[stateIndex];
stateIndex = (stateIndex + 1) % states.length;
if (spinHandle) {
queueUpdate();
}
});
}
function stopSpinning() {
updateMax();
cancelAnimationFrame(spinHandle);
spinHandle = 0;
if (spinner) {
spinner.textContent = "Done";
console.log(`Max delay between frames: ${maxDelay}ms`);
}
}
function updateMax() {
if (intervalStart !== 0) {
const elapsed = Date.now() - intervalStart;
if (elapsed > maxDelay) {
maxDelay = elapsed;
}
}
}
<div>(Look in the real browser console.)</div>
<input type="button" id="btn-go" value="Go">
<div id="spinner"></div>
<script type="worker" id="worker">
const r = Math.random;
self.addEventListener("message", ({data}) => {
if ((data && data.action) === "go") {
for (let n = 0; n < 1600; ++n) {
const nums = Float64Array.from(
{length: Math.floor(r() * 7000) * 3},
() => r()
);
postMessage({
action: "data",
array: nums,
done: n === 1600 - 1
}, [nums.buffer]);
}
}
});
</script>
That dramatically reduces the clock time to receive the data, while keeping the UI fairly responsive.

Async task manager with maximum number of concurrent "running" tasks

I am trying to implement two classes that can deal with asynchronous tasks in JavaScript:
Class Task: mimics the execution of a task with setTimeout. Once the timer expires, the task is considered completed.
Class TaskManager: has a capacity parameter to limit the numbers of tasks that can be executing in parallel.
I thought if I could just call the loop function recursively, just to keep checking if one job is done, I could proceed to the next job. But this leads immediately to a "Maximum call stack size exceeded" error.
Can someone explain how I can fix this?
class Task {
constructor(time) {
this.time = time;
this.running = 0;
}
run(limit, jobs, index) {
setTimeout(() => {
console.log('hello', index);
this.done(limit, jobs, index);
}, this.time);
}
done(limit, jobs, index) {
jobs.splice(index, 1);
console.log(jobs);
}
}
class TaskManager {
constructor(capacity) {
this.capacity = capacity;
this.jobs = [];
this.index = 0;
this.running = 0;
this.pending = [];
}
push(tk) {
this.jobs.push(tk);
this.index += 1;
const loop = () => {
if (this.jobs.length === 0) {
return;
}
if (this.jobs.length <= this.capacity) {
this.running += 1;
tk.run(this.capacity, this.jobs, this.index-1);
return;
}
loop();
}
loop();
}
}
const task = new Task(100);
const task1 = new Task(200);
const task2 = new Task(400);
const task3 = new Task(5000);
const task4 = new Task(6000);
const manager = new TaskManager(3);
manager.push(task);
manager.push(task1);
manager.push(task2);
manager.push(task3);
manager.push(task4);
You should not implement the busy loop, as that will block the event loop and so no user UI events or setTimeout events will be processed.
Instead respond to asynchronous events.
If you let the setTimeout callback resolve a Promise, it is not so hard to do.
I modified your script quite drastically. Here is the result:
class Task {
constructor(id, time) {
this.id = id;
this.time = time;
}
run() {
console.log(this + ' launched.');
return new Promise(resolve => {
setTimeout(() => {
console.log(this + ' completed.');
resolve();
}, this.time);
});
}
toString() {
return `Task ${this.id}[${this.time}ms]`;
}
}
class TaskManager {
constructor(capacity) {
this.capacity = capacity;
this.waiting = [];
this.running = [];
}
push(tk) {
this.waiting.push(tk);
if (this.running.length < this.capacity) {
this.next();
} else {
console.log(tk + ' put on hold.');
}
}
next() {
const task = this.waiting.shift();
if (!task) {
if (!this.running.length) {
console.log("All done.");
}
return; // No new tasks
}
this.running.push(task);
const runningTask = task.run();
console.log("Currently running: " + this.running);
runningTask.then(() => {
this.running = this.running.filter(t => t !== task);
console.log("Currently running: " + this.running);
this.next();
});
}
}
const a = new Task('A', 100);
const b = new Task('B', 200);
const c = new Task('C', 400);
const d = new Task('D', 5000);
const e = new Task('E', 6000);
const manager = new TaskManager(3);
manager.push(a);
manager.push(b);
manager.push(c);
manager.push(d);
manager.push(e);

rxjs: observable.complete is not a function

In the following piece of code I try to demonstrate how concatMap preserves the order of events, even if the action performed on events do not complete in order.
Now I get the error that
delayerObservable.complete() is not a function
This basically is just taken from a tutorial. I call next(), and then I call complete(). It should work, at least that's what I thought.
I can achive the desired functionality by returning randomDelayer.first()
return randomDelayer.first()
but I would like to complete the observable from within, since I might want to send out more events than just one.
const myTimer = Rx.Observable.create((observer) => {
let counter = 0;
setInterval( () => {
observer.next(counter++);
console.log('called next with counter: '+ counter);
},2000);
});
const myRandomDelayer = myTimer.concatMap( (value) => {
const randomDelayer = Rx.Observable.create( (delayerObservable) => {
const delay = Math.floor(Math.random()*1000);
setTimeout(() => {
console.log(delayerObservable);
delayerObservable.next('Hello, I am Number ' + value + ' and this was my delay: ' + delay);
delayerObservable.complete(); // <<-- this does not work (not a function)
}, delay);
});
return randomDelayer;
});
myRandomDelayer.subscribe( (message) => {
console.log(message);
});
It seems that there are quite a few changes between version 4 and 6 of the rxjs-framework. The working version of the defective source is this:
const { Observable } = rxjs;
const { map, filter, concatMap, pipe } = rxjs.operators;
console.log('Starting....');
const myTimer = Observable.create((observer) => {
let counter = 0;
setInterval( () => {
counter++;
if (counter < 10){
console.log('nexting now with counter ' + counter);
observer.next(counter);
} else {
observer.complete();
}
},1000);
});
const myRandomDelayer = myTimer.pipe(
concatMap( (value) => {
const randomDelayer = Observable.create( (delayerObservable) => {
const delay = Math.floor(Math.random()*5000);
setTimeout(() => {
delayerObservable.next('Hello, I am Number ' + value + ' and this was my delay: ' + delay);
delayerObservable.complete();
}, delay);
});
return randomDelayer;
})
);
myRandomDelayer.subscribe( (message) => {
console.log(message);
});

Categories