Reset timeout on event with RxJS - javascript

I'm experimenting with RxJS (with the JQuery extension) and I'm trying to solve the following use case:
Given that I have two buttons (A & B) I'd like to print a message if a certain "secret combination" is clicked within a given timeframe. For example the "secret combination" could be to click "ABBABA" within 5 seconds. If the combination is not entered within 5 seconds a timeout message should be displayed. This is what I currently have:
var secretCombination = "ABBABA";
var buttonA = $("#button-a").clickAsObservable().map(function () { return "A"; });
var buttonB = $("#button-b").clickAsObservable().map(function () { return "B"; });
var bothButtons = Rx.Observable.merge(buttonA, buttonB);
var outputDiv = $("#output");
bothButtons.do(function (buttonName) {
outputDiv.append(buttonName);
}).bufferWithTimeOrCount(5000, 6).map(function (combination) {
return combination.reduce(function (combination, buttonName) {
return combination + buttonName;
}, "");
}).map(function (combination) {
return combination === secretCombination;
}).subscribe(function (successfulCombination) {
if (successfulCombination) {
outputDiv.html("Combination unlocked!");
} else {
outputDiv.html("You're not fast enough, try again!");
}
});
While this works fairly well it's not exactly what I want. I need the bufferWithTimeOrCount to be reset when button A is pressed for the first time in a new timeframe. What I'm looking for is that as soon as the secret combination is pressed (ABBABA) I'd like "Combination unlocked!" to be shown (I don't want to wait for the time window to be expired).

Throttle is the typical operator for the delaying with reactive resetting effect you want.
Here's how you can use throttle in combination with scan to gather the combination inputted before the 5 seconds of silence:
var evaluationStream = bothButtons
.merge(bothButtons.throttle(5000).map(function(){return "reset";})) // (2) and (3)
.scan(function(acc, x) { // (1)
if (x === "reset") return "";
var newAcc = acc + x;
if (newAcc.length > secretCombination.length) {
return newAcc.substr(newAcc.length - secretCombination.length);
}
else {
return newAcc;
}
})
.map(function(combination) {
return combination === secretCombination;
});
var wrongStream = evaluationStream
.throttle(5000)
.filter(function(result) { return result === false; });
var correctStream = evaluationStream
.filter(function(result) { return result === true; });
wrongStream.subscribe(function() {
outputDiv.html("Too slow or wrong!");
});
correctStream.subscribe(function() {
outputDiv.html("Combination unlocked!");
});
(1) We scan to concatenate the input characters. (2) Throttle waits for 5 seconds of event silence and emits the last event before that silence. In other words, it's similar to delay, except it resets the inner timer when a new event is seen on the source Observable. We need to reset the scan's concatenation (1), so we just map the same throttled Observable to "reset" flags (3), which the scan will interpret as clearing the accumulator (acc).
And here's a JSFiddle.

Related

matter.js: collisionStart triggered many times for one collision

I am working on a game app using React native and Matter.js.
I am trying to implement a system that adds points every time a bullet hits a target.
In order to do this, I am trying to use collisionStart to detect the collision.
However, even though the bullet and target collide only once, the event seems to be triggered 41 times.
This is the code:
Matter.Events.on(engine, 'collisionStart', (event) => {
let pairs = event.pairs
for (const pair of pairs) {
if (pair.bodyA.label === 'bullet' && pair.bodyB.label === 'Worm') {
console.log("target hit");
}
}
})
In the end, I'm planning to replace console.log with something that adds points. At the current moment, one collision seems like it would trigger the add points 41 times, which is obviously not ideal.
Any ideas what is happening here and how I can get this to trigger only once for one collision?
Try next example. I take it from my own project [you need little adaptd from ts to js]:
Matter.Events.on(this.starter.getEngine(), "collisionStart", function (event) {
root.collisionCheck(event, true);
});
public collisionCheck(event, ground: boolean) {
const myInstance = this;
const pairs = event.pairs;
for (let i = 0, j = pairs.length; i !== j; ++i) {
const pair = pairs[i];
if (pair.activeContacts) {
if (pair.bodyA.label === "bullet" && pair.bodyB.label === "Worm") {
const collectitem = pair.bodyA;
this.playerDie(collectitem);
} else if (pair.bodyB.label === "bullet" && pair.bodyA.label === "Worm") {
const collectitem = pair.bodyB;
this.playerDie(collectitem);
}
// ....
}
}
}
public destroyBody = (destroyBody) => {
try {
Matter.Composite.remove(this.getWorld(), destroyBody);
} catch(err) {
console.log(err)
}
}
If you still have same problem , we can adapt also with flag PREVENT_DOUBLE_BY_1_SECOUND for example.

Javascript: Infinite loop in webworker [duplicate]

I want to sort an array, using Web Workers. But this array might receive new values over time, while the worker is still performing the sort function.
So my question is, how can I "stop" the sorting computation on the worker after receiving the new item, so it can perform the sort on the array with that item, while still keeping the sorting that was already made?
Example:
let worker = new Worker('worker.js');
let list = [10,1,5,2,14,3];
worker.postMessage({ list });
setInterval(() => worker.postMessage({ num: SOME_RANDOM_NUM, list }), 100);
worker.onmessage = event => {
list = event.data.list;
}
So lets say that, I've passed 50, the worker made some progress in the sorting before that and now I have something like this:
[1, 2, 3, 10, 5, 14, 50]. Which means the sorting stopped at index 3. So I pass this new array back to the worker, so it can continue the sorting from position 3.
How can I accomplish that, since there is no way to pause/resume a web worker?
Even though the Worker works on an other thread than the one of your main page, and can thus run continuously without blocking the UI, it still runs on a single thread.
This means that until your sort algorithm has finished, the Worker will delay the execution of the message event handler; it is as blocked as would be the main thread.
Even if you made use of an other Worker from inside this worker, the problem would be the same.
The only solution would be to use a kind of generator function as the sorter, and to yield it every now and then so that the events can get executed.
But doing this will drastically slow down your sorting algorithm.
To make it better, you could try to hook to each Event Loop, thanks to a MessageChannel object: you talk in one port and receive the message in the next Event loop. If you talk again to the other port, then you have your own hook to each Event loop.
Now, the best would be to run a good batch in every of these Event loop, but for demo, I'll call only one instance of our generator function (that I borrowed from this Q/A)
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let sorter = bubbleSort(list);
let done = false;
/* inner messaging channel */
const msg_channel = new MessageChannel();
// Hook to every Event loop
msg_channel.port2.onmessage = e => {
// procede next step in sorting algo
// could be a few thousands in a loop
const state = sorter.next();
// while running
if(!state.done) {
msg_channel.port1.postMessage('');
done = false;
}
else {
done = true;
}
}
msg_channel.port1.postMessage("");
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
sorter = bubbleSort(list);
msg_channel.port1.postMessage('');
}
}
};
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
Note that the same can be achieved with an async function, which may be more practical in some cases:
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let done = false;
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
There are two decent options.
Option 1: Worker.terminate()
The first is just to kill your existing web worker and start a new one. For that you can use Worker.terminate().
The terminate() method of the Worker interface immediately terminates the Worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
The only downsides of this approach are:
You lose all worker state. If you had to copy a load of data into it for the request you have to do it all again.
It involves thread creation and destruction, which isn't as slow as most people think but if you terminate web workers a lot it might cause issues.
If neither of those are an issue it is probably the easiest option.
In my case I have lots of state. My worker is rendering part of an image, and when the user pans to a different area I want it to stop what it is doing and start rendering the new area. But the data needed to render the image is pretty huge.
In your case you have the state of your (presumably huge) list that you don't want to use.
Option 2: Yielding
The second option is basically to do cooperative multitasking. You run your computation as normal, but every now and then you pause (yield) and say "should I stop?", like this (this is for some nonsense calculation, not sorting).
let requestId = 0;
onmessage = event => {
++requestId;
sortAndSendData(requestId, event.data);
}
function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This won't work though because sortAndSendData() runs to completion and blocks the web worker's event loop. We need some way to yield just before thisRequestId !== requestId. Unfortunately Javascript doesn't quite have a yield method. It does have async/await so we might try this:
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await Promise.resolve();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
Unfortunately it doesn't work. I think it's because async/await executes things eagerly using "microtasks", which get executed before pending "macrotasks" (our web worker message) if possible.
We need to force our await to become a macrotask, which you can do using setTimeout(0):
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
function yieldToMacrotasks() {
return new Promise((resolve) => setTimeout(resolve));
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await yieldToMacrotasks();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This works! However it is extremely slow. await yieldToMacrotasks() takes approximately 4 ms on my machine with Chrome! This is because browsers set a minimum timeout on setTimeout(0) of something like 1 or 4 ms (the actual minimum seems to be complicated).
Fortunately another user pointed me to a quicker way. Basically sending a message on another MessageChannel also yields to the event loop, but isn't subject to the minimum delay like setTimeout(0) is. This code works and each loop only takes ~0.04 ms which should be fine.
let currentTask = {
cancelled: false,
}
onmessage = event => {
currentTask.cancelled = true;
currentTask = {
cancelled: false,
};
performComputation(currentTask, event.data);
}
async function performComputation(task, data) {
let total = 0;
let promiseResolver;
const channel = new MessageChannel();
channel.port2.onmessage = event => {
promiseResolver();
};
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Yield to the event loop.
const promise = new Promise(resolve => {
promiseResolver = resolve;
});
channel.port1.postMessage(null);
await promise;
// Check if this task has been superceded by another one.
if (task.cancelled) {
return;
}
}
// Return the result.
postMessage(total);
}
I'm not totally happy about it - it relies on postMessage() events being processed in FIFO order, which I doubt is guaranteed. I suspect you could rewrite the code to make it work even if that isn't true.
You can do it with some trick – with the help of setTimeout function interrupting. For example it is not possible without an addition thread to execute 2 functions parallel, but with setTimeout function interrupting trick we can do it like follows:
Example of parallel execution of functions
var count_0 = 0,
count_1 = 0;
function func_0()
{
if(count_0 < 3)
setTimeout(func_0, 0);//the same: setTimeout(func_0);
console.log('count_0 = '+count_0);
count_0++
}
function func_1()
{
if(count_1 < 3)
setTimeout(func_1, 0);
console.log('count_1 = '+count_1)
count_1++
}
func_0();
func_1();
You will get this output:
count_0 = 0
count_1 = 0
count_0 = 1
count_1 = 1
count_0 = 2
count_1 = 2
count_0 = 3
count_1 = 3
Why is it possible? Because the setTimeout function needs some time to be executed. And this time is even enought for the execution of some part from your following code.
Solution for you
For this case you have to write your own array sort function (or you can also use the following function from me) because we can not interrupt the native sort function. And in this your own function you have to use this setTimeout function interrupting trick. And you can receive your message event notification.
In the following example I have the interrupting in the half length of my array, and you can change it if you want.
Example with custom sort function interrupting
var numbers = [4, 2, 1, 3, 5];
// this is my bubble sort function with interruption
/**
* Sorting an array. You will get the same, but sorted array.
* #param {array[]} arr – array to sort
* #param {number} dir – if dir = -1 you will get an array like [5,4,3,2,1]
* and if dir = 1 in opposite direction like [1,2,3,4,5]
* #param {number} passCount – it is used only for setTimeout interrupting trick.
*/
function sortNumbersWithInterruption(arr, dir, passCount)
{
var passes = passCount || arr.length,
halfOfArrayLength = (arr.length / 2) | 0; // for ex. 2.5 | 0 = 2
// Why we need while loop: some values are on
// the end of array and we have to change their
// positions until they move to the first place of array.
while(passes--)
{
if(!passCount && passes == halfOfArrayLength)
{
// if you want you can also not write the following line for full break of sorting
setTimeout(function(){sortNumbersWithInterruption(arr, dir, passes)}, 0);
/*
You can do here all what you want. Place 1
*/
break
}
for(var i = 0; i < arr.length - 1; i++)
{
var a = arr[i],
b = arr[i+1];
if((a - b) * dir > 0)
{
arr[i] = b;
arr[i+1] = a;
}
}
console.log('array is: ' + arr.join());
}
if(passCount)
console.log('END sring is: ' + arr.join());
}
sortNumbersWithInterruption(numbers, -1); //without passCount parameter
/*
You can do here all what you want. Place 2
*/
console.log('The execution is here now!');
You will get this output:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
You can do it with insertion sort (kind of).
Here is the idea:
Start your worker with an internal empty array (empty array is sorted obviously)
Your worker receives only elements not the entire array
Your worker insert any received element right in correct position into the array
Every n seconds, the worker raises a message with the current array if it has changed after the last event. (If you prefer, you can send the array on every insertion, but is more efficient to buffer somehow)
Eventually, you get the entire array, if any item is added, you will receive the updated array to.
NOTE: Because your array is always sorted, you can insert in correct position using binary search. This is very efficient.
I think the case comes down to careful management of postMessage calls and amount of data passed to be processed at a time. Was dealing with problem of this kind - think about not sending all new data into the function at once but rather creating your own queue and when small enough portion of the task has been acomplished by webworker thread send a message back to the main thread and decide to send the next portion, wait or quit.
In Your case, e.g. one time You get 9000 new items, next 100k - maybe create a queue/buffer that adds next 10k new elements each time webworker is done processing last data change.
const someWorker = new Worker('abc.js');
var processingLock = false;
var queue = [];
function newDataAction(arr = null) {
if (arr != null) {
queue = queue.concat(arr);
}
if (!processingLock) {
processingLock = true;
var data = [];
for (let i = 0; i < 10000 && queue.length > 0; i++) {
data.push(queue.pop());
}
worker.postMessage(data);
}
}
someWorker.addEventListener('message', function(e) {
if (e.data == 'finished-last-task') {
processingLock = false;
if (queue.length > 0) {
newDataAction();
}
}
});
Worked through many sorting algorithms and I don't see how sending new data into an sorting algorithm with partially sorted array makes much difference in terms of compuation time from sorting them both sequentially and performing a merge.

Problem with infinite loop when manipulating DOM

I'm learning about DOM manipulation and, to practice, I'm trying to get the first 100 Twitter users who have twitted about #Javascript (see link). As, for now, Twitter doesn't allow you to use console.log() function in the browser console, I have managed to show any string visually in the HTML, in this case, under the search textbox.
This is my "custom" console.log()
function consoleLog(data) {
var searchTextBox = document.querySelector("#doc > div.topbar.js-topbar > div > div > div > div > div");
var p = document.createElement("p");
var innerText = document.createTextNode(data);
p.appendChild(innerText);
searchTextBox.appendChild(p);
}
For getting the usernames, I keep scrolling the page every 4 seconds and looking for usernames until I have 100 or more of them in my usernames variable.
var scrollPage = setInterval(function() {
window.scrollTo(0, document.body.scrollHeight);
}, 4000);
var usernames = [];
while (true) { // <------ PROBLEM
if (usernames.length < 100) {
consoleLog("Getting usernames again");
usernames = getUsernames();
}
else {
consoleLog("We have ENOUGH usernames. BREAK");
clearInterval(scrollPage);
printUsernames();
break;
}
}
function printUsernames() {
for(var user of usernames) {
consoleLog(user);
}
}
function getUsernames() {
var results = [];
var usernameNodes = document.getElementsByClassName("username u-dir u-textTruncate");
var username = usernameNodes[0].textContent;
for(var node of usernameNodes) {
results.push(node.textContent);
}
return results.filter(isUnique);
}
function isUnique(value, index, self) {
return self.indexOf(value) === index;
}
The problem is that the while loop enters in infinte loop and I don't know why. I think the logic of the code is correct. In fact, if I first copy and paste all the declared functions to the browser console, then start the scrollPage interval and, lastly, start the while loop, it works well. The problem comes when I copy and paste all the code at one time in the browser console. It is like the executions of the interval and the while loop conflict in some way. But I can't understand.
Its better to have while conditioned like this:
var usernames = [];
// This will automatically end when length is greater or equall 100
// no need to break
while (usernames.length < 100) {
consoleLog("Getting usernames again");
usernames = getUsernames();
}
consoleLog("We have ENOUGH usernames.");
clearInterval(scrollPage);
printUsernames();

A pattern to queue notifications?

I created a library to pop up some toast notifications and I tried to put a limit on the maximum notifications on screen.
I managed to extract the idea into a plunker (don't mind the code, it is only to solve the issue).
I have a function to create those toasts:
function createToast() {
var body = $document.find('body').eq(0);
var toast = {};
toast.id = index++;
toast.el = angular.element('<div class="toast">Toast ' + toast.id + '</div>');
toast.el = $compile(toast.el)($scope);
if (maxOpened && toasts.length >= maxOpened) {
remove(toasts[0].id);
}
toasts.push(toast);
$animate.enter(toast.el, body).then(function() {
$timeout(function() {
remove(toast.id);
}, 3000);
});
}
Basically it creates a new object with an el and then animates it out on the body. Notice that if the maxOpened is reached it removes the first one.
function remove(id) {
var toast = findToast(id);
if (toast) {
$animate.leave(toast.el).then(function() {
var index = toasts.indexOf(toast);
toasts.splice(index, 1);
});
}
function findToast(toastId) {
for (var i = 0; i < toasts.length; i++) {
if (toasts[i].id === id) {
return toasts[i];
}
}
}
}
Find the toast, animate the leave and then delete it.
If I do a $interval on them, let's say 600ms it works.
Try here: http://plnkr.co/edit/lDnT57FPadCt5Ir5wHuK?p=preview
If you lower it to something like 100ms it starts to break, not only ignoring the max but also leaving some orphan toasts that won't get deleted.
So I am not sure what could be a good solution here. My best guess is to provide a queue so I start to drain it as soon as a toast get removed but so far, I didn't make it.
The probably simplest solution would be to add a deferred to each toast and only start to animate the toast when the limit is not or no longer reached.
You start by adding a deferred and resolve it immediately, if the limit is not reached yet or the limit can be ignored:
toast.slotOpen = $q.defer();
toasts.push(toast);
if (maxOpened && toasts.length <= maxOpened || !maxOpened) { // i guess 0 or a falsy value means to ignore the limit
toast.slotOpen.resolve();
}
You only start the animation, when a slot is open:
toast.slotOpen.promise.then(function() {
$animate.enter(toast.el, body).then(function() {
The last thing to do is to resolve the deferred when a new slot gets opened after an old toast has been removed:
$animate.leave(toast.el).then(function() {
var index = toasts.indexOf(toast);
toasts.splice(index, 1);
if (maxOpened && toasts.length >= maxOpened) {
toasts[maxOpened - 1].slotOpen.resolve();
}
I have adjusted your code and created a new Plunker.

How to deal with asyncronous javascript in loops?

I have a forloop like this:
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname);
myphone.get(function(phonenumbers){
if(myphone.phonearray){
myperson.save();
//Can I put a break here?;
}
});
}
What it does is that it searches for phone-numbers in a database based on various first-names. What I want to achieve is that once it finds a number associated with any of the first names, it performs myperson.save and then stops all the iterations, so that no duplicates get saved. Sometimes, none of the names return any phone-numbers.
myphone.get contains a server request and the callback is triggered on success
If I put a break inside the response, what will happen with the other iterations of the loop? Most likely the other http-requests have already been initiated. I don't want them to perform the save. One solution I have thought of is to put a variable outside of the forloop and set it to save, and then check when the other callbacks get's triggered, but I'm not sure if that's the best way to go.
You could write a helper function to restrict invocations:
function callUntilTrue(cb) {
var done = false;
return function () {
if (done) {
log("previous callback succeeded. not calling others.");
return;
}
var res = cb.apply(null, arguments);
done = !! res;
};
}
var myperson = {
firstname: {
"tom": null,
"jerry": null,
"micky": null
},
save: function () {
log("save " + JSON.stringify(this, null, 2));
}
};
var cb = function (myperson_, phonenumbers) {
if (myperson_.phonearray) {
log("person already has phone numbers. returning.");
return false;
}
if (phonenumbers.length < 1) {
log("response has no phone numbers. returning.");
return false;
}
log("person has no existing phone numbers. saving ", phonenumbers);
myperson_.phonearray = phonenumbers;
myperson_.save();
return true;
};
var restrictedCb = callUntilTrue(cb.bind(null, myperson));
for (var name in myperson.firstname) {
var myphone = new phone(myperson, name);
myphone.get(restrictedCb);
}
Sample Console:
results for tom-0 after 1675 ms
response has no phone numbers. returning.
results for jerry-1 after 1943 ms
person has no existing phone numbers. saving , [
"jerry-1-0-number"
]
save {
"firstname": {
"tom": null,
"jerry": null,
"micky": null
},
"phonearray": [
"jerry-1-0-number"
]
}
results for micky-2 after 4440 ms
previous callback succeeded. not calling others.
Full example in this jsfiddle with fake timeouts.
EDIT Added HTML output as well as console.log.
The first result callback will only ever happen after the loop, because of the single-threaded nature of javascript and because running code isn't interrupted if events arrive.
If you you still want requests to happen in parallel, you may use a flag
var saved = false;
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
myphone.get(function(phonenumbers){
if (!saved && myphone.phonearray){
saved = true;
myperson.save();
}
});
}
This will not cancel any pending requests, however, just prevent the save once they return.
It would be better if your .get() would return something cancelable (the request itself, maybe).
var saved = false;
var requests = [];
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
var r;
requests.push(r = myphone.get(function(phonenumbers){
// Remove current request.
requests = requests.filter(function(i) {
return r !== i;
});
if (saved || !myphone.phonearray) {
return;
}
saved = true;
// Kill other pending/unfinished requests.
requests.forEach(function(r) {
 r.abort();
});
myperson.save();
}));
}
Even better, don't start all requests at once. Instead construct an array of all possible combinations, have a counter (a semaphore) and only start X requests.
var saved = false;
var requests = [];
// Use requests.length as the implicit counter.
var waiting = []; // Wait queue.
for (var name in myperson.firstname){
var myphone = new phone(myperson, firstname /* name? */);
var r;
if (requests.length >= 4) {
// Put in wait queue instead.
waiting.push(myphone);
continue;
}
requests.push(r = myphone.get(function cb(phonenumbers){
// Remove current request.
requests = requests.filter(function(i) {
return r !== i;
});
if (saved) {
return;
}
if (!myphone.phonearray) {
// Start next request.
var w = waiting.shift();
if (w) {
requests.push(w.get(cb));
)
return;
}
saved = true;
// Kill other pending/unfinished requests.
requests.forEach(function(r) {
r.abort();
});
myperson.save();
}));
}

Categories

Resources