Stop long running javascript functions on the browser - javascript

My question has two parts
I want to terminate a function if it runs more than 200ms and return a default value. I tried to experiment with the concept of Promise.race but that did not yield the desired result
I want to identify if the function has an infinite loop similiar to how JSFiddle or Codepen is able to exit when someone uses an infinite loop without freezing the browser or overloading the CPU
Is it possible

Yeah, you can do that, here's a general-purpose script I wrote as proof of concept. It dynamically creates a web worker and uses it in the same way other languages have threads. However, if you're just trying to kill an active XHR or Fetch call, there are built-in methods for doing that.
/**
* Run some code in it's own thread
* #params - Any parameters you want to be passed into the thread
* #param - The last parameter must be a function that will run in it's own thread
* #returns - An promise-like object with a `then`, `catch`, and `abort` method
*/
function Thread() {
var worker;
const promise = new Promise((resolve, reject) => {
var args = Array.from(arguments);
var func = args.pop();
if (typeof func !== "function") throw new Error("Invalid function");
var fstr = func.toString();
var mainBody = fstr.substring(fstr.indexOf("{") + 1, fstr.lastIndexOf("}"));
var paramNames = fstr.substring(fstr.indexOf("(") + 1, fstr.indexOf(")")).split(",").map(p => p.trim());
var doneFunct = paramNames.pop();
if (paramNames.length !== args.length) throw new Error("Invalid number of arguments.");
var workerStr = `var ${doneFunct} = function(){
var args = Array.from(arguments);
postMessage(args);
};
self.onmessage = function(d){
var [${paramNames.join(", ")}] = d.data;
${mainBody}
};`;
var blob = new Blob([workerStr], {
type: 'application/javascript'
});
worker = new Worker(URL.createObjectURL(blob));
worker.onerror = reject;
worker.onmessage = (d) => {
resolve(...d.data);
worker.terminate();
};
worker.postMessage(args);
});
return {
then: (...params)=>promise.then(...params),
catch: (...params)=>promise.catch(...params),
abort: ()=>worker.terminate()
}
}
////////////////////////
//// EXAMPLE USAGE /////
////////////////////////
// the thread will take 2 seconds to execute
// and then log the result
var myThread = new Thread("this is a message", 2, function(message, delaySeconds, exit) {
setTimeout(() => {
exit(message.split('').reverse().join(''));
}, delaySeconds * 1000);
});
myThread.then(result => console.log(result));
// the thread will take 2 seconds to execute
// but we will cancel it after one second
var myThread = new Thread("this is a message", 2, function(message, delaySeconds, exit) {
setTimeout(() => {
exit(message.split('').reverse().join(''));
}, delaySeconds * 1000);
});
setTimeout(()=>{
myThread.abort();
}, 1000);

If you have access to the function you can add a time check inside the loop that runs for long time and return when you reach time limit (simplest way, most compatible way).
If you don't know what the function is doing or can't modify it, you can try wrapping it inside a Web worker. If you start the long running js inside the worker, you can then .terminate() it when it reaches the timeout
Also, It is impossible to determine if something contains an infinite loop without executing it

Related

Why does setTimeout behave this way in my stream implementation?

The final line of this code successfully calls the _read method of a custom Duplex stream in node.
const timeContext = new TimeContext(sampleRate);
const input = new InputStream(timeContext); // Stream.Readable
const throttle = new Throttle(sampleRate); // Stream.Transform
const stackSource = [];
const stack = new StackStream(stackSource); // Stream.Duplex
input.pipe(throttle).pipe(stack);
stack.read(); // This will call the _read method of StackStream
Adding setTimeout to delay the stream.read() call, setTimeout's callback does NOT get called:
const timeContext = new TimeContext(sampleRate);
const input = new InputStream(timeContext); // Stream.Readable
const throttle = new Throttle(sampleRate); // Stream.Transform
const stackSource = [];
const stack = new StackStream(stackSource); // Stack.Duplex
input.pipe(throttle).pipe(stack);
setTimeout(() => {
stack.read(); // This callback never gets called
}, 1000);
It definitely does get called but something else is erroring
setTimeout(() => {
console.log('We got here');
stack.read(); // This is what is crashing in your code
console.log('We don\'t get here');
}, 1000);
It is just not behaving as you expect because some other error is occurring. Look in the console to see what errors are raised.
Looks like, read() function is a local property of the stack object and the setTimeout is not able to see this local property of stack object. That's why it's behaving in such a way.
Refer this solution for reference,
https://stackoverflow.com/a/4536268/10371717

Using promise to work with web worker inside a JavaScript closure

I was executing an image processing operation in JavaScript which was working as expected expect one thing that sometimes it was freezing the UI, which made me to use Web worker to excute the image processing functions.
I have a scenario where i need to process multiple. Below is a summary of workflow which i am using to achieve the above feat.
//closure
var filter = (function(){
function process(args){
var promise = new Promise(function (resolve, reject) {
if (typeof (Worker) !== "undefined") {
if (typeof (imgWorker) == "undefined") {
imgWorker = new Worker("/processWorker.js");
}
imgWorker.postMessage(args);
imgWorker.onmessage = function (event) {
resolve(event.data);
};
} else {
reject("Sorry, your browser does not support Web Workers...");
}
});
return promise;
}
return {
process: function(args){
return process(args);
}
}
})();
function manipulate(args, callback){
filter.process(args).then(function(res){
callback(res);
});
}
Here, i am loading multiple images and passing them inside manipulate function.
The issue i am facing here in this scenario is that sometimes for few images Promise is not never resolved.
After debugging my code i figured out that it is because i am creating a Promise for an image while previous Promise was not resolved.
I need suggestions on how can i fix this issue, also i have another query should i use same closure(filter here in above scenario) multiple times or create new closure each time when required as below:
var filter = function(){
....
return function(){}
....
}
function manipulate(args, callback){
var abc = filter();
abc.process(args).then(function(res){
callback(res);
});
}
I hope my problem is clear, if not please comment.
A better approach would be to load your image processing Worker once only. during the start of your application or when it is needed.
After that, you can create a Promise only for the function you wish to call from the worker. In your case, filter can return a new Promise object every time that you post to the Worker. This promise object should only be resolved when a reply is received from the worker for the specific function call.
What is happening with your code is that, your promises are resolving even though the onmessage handler is handling a different message from the Worker. ie. if you post 2 times to the worker. if the second post returns a message it automatically resolves both of your promise objects.
I created a worker encapsulation here Orc.js. Although it may not work out of the box due to the fact i haven't cleaned it of some dependencies i built into it. Feel free to use the methods i applied.
Additional:
You will need to map your post and onmessage to your promises. this will require you to modify your Worker code as well.
//
let generateID = function(args){
//generate an ID from your args. or find a unique way to distinguish your promises.
return id;
}
let promises = {}
// you can add this object to your filter object if you like. but i placed it here temporarily
//closure
var filter = (function(){
function process(args){
let id = generateID(args)
promises[id] = {}
promises[id].promise = new Promise(function (resolve, reject) {
if (typeof (Worker) !== "undefined") {
if (typeof (imgWorker) == "undefined") {
imgWorker = new Worker("/processWorker.js");
imgWorker.onmessage = function (event) {
let id = generateID(event.data.args) //let your worker return the args so you can check the id of the promise you created.
// resolve only the promise that you need to resolve
promises[id].resolve(event.data);
}
// you dont need to keep assigning a function to the onmessage.
}
imgWorker.postMessage(args);
// you can save all relevant things in your object.
promises[id].resolve = resolve
promises[id].reject = reject
promises[id].args = args
} else {
reject("Sorry, your browser does not support Web Workers...");
}
});
//return the relevant promise
return promises[id].promise;
}
return {
process: function(args){
return process(args);
}
}
})();
function manipulate(args, callback){
filter.process(args).then(function(res){
callback(res);
});
}
typescirpt equivalent on gist:
Combining answers from "Webworker without external files"
you can add functions to worker scope like the line `(${sanitizeThis.toString()})(this);,` inside Blob constructing array.
There are some problems regarding resolving promise outside of the promise enclosure, mainly about error catching and stack traces, I didn't bother because it works perfectly fine for me right now.
// https://stackoverflow.com/a/37154736/3142238
function sanitizeThis(self){
// #ts-ignore
// console.assert(this === self, "this is not self", this, self);
// 'this' is undefined
"use strict";
var current = self;
var keepProperties = [
// Required
'Object', 'Function', 'Infinity', 'NaN',
'undefined', 'caches', 'TEMPORARY', 'PERSISTENT',
"addEventListener", "onmessage",
// Optional, but trivial to get back
'Array', 'Boolean', 'Number', 'String', 'Symbol',
// Optional
'Map', 'Math', 'Set',
"console",
];
do{
Object.getOwnPropertyNames(
current
).forEach(function(name){
if(keepProperties.indexOf(name) === -1){
delete current[name];
}
});
current = Object.getPrototypeOf(current);
} while(current !== Object.prototype);
}
/*
https://hacks.mozilla.org/2015/07/how-fast-are-web-workers/
https://developers.google.com/protocol-buffers/docs/overview
*/
class WorkerWrapper
{
worker;
stored_resolves = new Map();
constructor(func){
let blob = new Blob([
`"use strict";`,
"const _postMessage = postMessage;",
`(${sanitizeThis.toString()})(this);`,
`const func = ${func.toString()};`,
"(", function(){
// self.onmessage = (e) => {
addEventListener("message", (e) => {
_postMessage({
id: e.data.id,
data: func(e.data.data)
});
})
}.toString(), ")()"
], {
type: "application/javascript"
});
let url = URL.createObjectURL(blob);
this.worker = new Worker(url);
URL.revokeObjectURL(url);
this.worker.onmessage = (e) => {
let { id, data } = e.data;
let resolve = this.stored_resolves.get(id);
this.stored_resolves.delete(id);
if(resolve){
resolve(data);
} else{
console.error("invalid id in message returned by worker")
}
}
}
terminate(){
this.worker.terminate();
}
count = 0;
postMessage(arg){
let id = ++this.count;
return new Promise((res, rej) => {
this.stored_resolves.set(id, res);
this.worker.postMessage({
id,
data: arg
});
})
}
}
// usage
let worker = new WorkerWrapper(
(d) => { return d + d; }
);
worker.postMessage("HEY").then((e) => {
console.log(e); // HEYHEY
})
worker.postMessage("HELLO WORLD").then((f) => {
console.log(f); // HELLO WORLDHELLO WORLD
})
let worker2 = new WorkerWrapper(
(abc) => {
// you can insert anything here,
// just be aware of whether variables/functions are in scope or not
return(
{
"HEY": abc,
[abc]: "HELLO WORLD" // this particular line will fail with babel
// error "ReferenceError: _defineProperty is not defined",
}
);
}
);
worker2.postMessage("HELLO WORLD").then((f) => {
console.log(f);
/*
{
"HEY": "HELLO WORLD",
"HELLO WORLD": "HELLO WORLD"
}
*/
})
/*
observe how the output maybe out of order because
web worker is true async
*/

How to create your own setTimeout function?

I understand how to use setTimeout function, but I can't find a way to create a function like it.
I have an example:
setTimeout(() => {
console.log('3s');
}, 3000);
while(1);
The result is setTimeout callback never call so I think it use the same thread like every js other functions. But when it's check the time reach or not? and how it can do that?
Updated
To avoid misunderstanding I update my question.
I can't find a way to create a async function with callback after specify time (without using setTimeout and don't block entire thread). This function setTimeout seen like a miracle to me. I want to understand how it work.
Just for the game since I really don't see why you couldn't use setTimeout...
To create a non-blocking timer, without using the setTimeout/setInterval methods, you have only two ways:
event based timer
run your infinite loop in a second thread
Event based timer
One naive implementation would be to use the MessageEvent interface and polling until the time has been reached. But that's not really advice-able for long timeouts as this would force the event-loop to constantly poll new tasks, which is bad for trees.
function myTimer(cb, ms) {
const begin = performance.now();
const channel = myTimer.channel ??= new MessageChannel();
const controller = new AbortController();
channel.port1.addEventListener("message", (evt) => {
if(performance.now() - begin >= ms) {
controller.abort();
cb();
}
else if(evt.data === begin) channel.port2.postMessage(begin);
}, { signal: controller.signal });
channel.port1.start();
channel.port2.postMessage(begin);
}
myTimer(() => console.log("world"), 2000);
myTimer(() => console.log("hello"), 100);
So instead, if available, one might want to use the Web Audio API and the AudioScheduledSourceNode, which makes great use of the high precision Audio Context's own clock:
function myTimer(cb, ms) {
if(!myTimer.ctx) myTimer.ctx = new (window.AudioContext || window.webkitAudioContext)();
var ctx = myTimer.ctx;
var silence = ctx.createGain();
silence.gain.value = 0;
var note = ctx.createOscillator();
note.connect(silence);
silence.connect(ctx.destination);
note.onended = function() { cb() };
note.start(0);
note.stop(ctx.currentTime + (ms / 1000));
}
myTimer(()=>console.log('world'), 2000);
myTimer(()=>console.log('hello'), 200);
Infinite loop on a different thread
Yes, using Web Workers we can run infinite loops without killing our web page:
function myTimer(cb, ms) {
var workerBlob = new Blob([mytimerworkerscript.textContent], {type: 'application/javascript'});
var url = URL.createObjectURL(workerBlob);
var worker = new Worker(url);
worker.onmessage = function() {
URL.revokeObjectURL(url);
worker.terminate();
cb();
};
worker.postMessage(ms);
}
myTimer(()=>console.log('world'), 2000);
myTimer(()=>console.log('hello'), 200);
<script id="mytimerworkerscript" type="application/worker-script">
self.onmessage = function(evt) {
var ms = evt.data;
var now = performance.now();
while(performance.now() - now < ms) {}
self.postMessage('done');
}
</script>
And for the ones who like to show off they know about the latest features not yet really available (totally not my style), a little mention of the incoming Prioritized Post Task API and its delayed tasks, which are basically a more powerful setTimeout, returning a promise, on which we can set prioritization.
(async () => {
if(globalThis.scheduler) {
const p1 = scheduler.postTask(()=>{ console.log("world"); }, { delay: 2000} );
const p2 = scheduler.postTask(()=>{ console.log("hello"); }, { delay: 1000} );
await p2;
console.log("future");
}
else {
console.log("Your browser doesn't support this API yet");
}
})();
The reason callback of setTimeout() is not being called is, you have while(1) in your code which acts as infinite loop. It will keep your javascript stack busy whole time and that is the reason event loop will never push callback function of setTimeout() in stack.
If you remove while(1) from your code, callback for setTimeout() should get invoked.
setTimeout(() => {
console.log('3s');
}, 3000);
To create your own setTimeout function, you can use the following function, setMyTimeout() to do that without using setTimeout.
var foo= ()=>{
console.log(3,"Called after 3 seconds",new Date().getTime());
}
var setMyTimeOut = (foo,timeOut)=>{
let timer;
let currentTime = new Date().getTime();
let blah=()=>{
if (new Date().getTime() >= currentTime + timeOut) {
clearInterval(timer);
foo()
}
}
timer= setInterval(blah, 100);
}
console.log(1,new Date().getTime());
setMyTimeOut(foo,3000)
console.log(2,new Date().getTime());
Following is the implementation of custom setTimeout and setInterval, clearTimeout and clearInterval. I created them to use in sandbox environments where builtin setTimeout and setInterval doesn't work.
const setTimeouts = [];
export function customSetTimeout(cb, interval) {
const now = window.performance.now();
const index = setTimeouts.length;
setTimeouts[index] = () => {
cb();
};
setTimeouts[index].active = true;
const handleMessage = (evt) => {
if (evt.data === index) {
if (window.performance.now() - now >= interval) {
window.removeEventListener('message', handleMessage);
if (setTimeouts[index].active) {
setTimeouts[index]();
}
} else {
window.postMessage(index, '*');
}
}
};
window.addEventListener('message', handleMessage);
window.postMessage(index, '*');
return index;
}
export function customClearTimeout(setTimeoutId) {
if (setTimeouts[setTimeoutId]) {
setTimeouts[setTimeoutId].active = false;
}
}
const setIntervals = [];
export function customSetInterval(cb, interval) {
const intervalId = setIntervals.length;
setIntervals[intervalId] = function () {
if (setIntervals[intervalId].active) {
cb();
customSetTimeout(setIntervals[intervalId], interval);
}
};
setIntervals[intervalId].active = true;
customSetTimeout(setIntervals[intervalId], interval);
return intervalId;
}
export function customClearInterval(intervalId) {
if (setIntervals[intervalId]) {
setIntervals[intervalId].active = false;
}
}
Hi you can try this. ]
HOpe it will help. Thanks
function customSetTimeOut (callback, ms) {
var dt = new Date();
var i = dt.getTime();
var future = i + ms;
while(Date.now() <= future) {
//do nothing - blocking
}
return callback();
}
customSetTimeOut(function(){
console.log("Timeout success");
},1000);

How to delay execution of functions, JavaScript

Background
I am trying to create a factory function that executes a specific async function with a given delay.
For the purposes of this question, this will be the async function I refer to:
/*
* This is a simulation of an async function. Be imaginative!
*/
let asyncMock = function(url) {
return new Promise(fulfil => {
setTimeout(() => {
fulfil({
url,
data: "banana"
});
}, 10000);
});
};
This function takes an url and it returns a JSON object containing that URL and some data.
All around my code, I have this function called in the following way:
asyncMock('http://www.bananas.pt')
.then(console.log);
asyncMock('http://www.berries.com')
.then(console.log);
//... badjillion more calls
asyncMock('http://www.oranges.es')
.then(console.log);
Problem
The problem here is that all these calls are made at exactly the same time, thus overloading the resources that asyncMoc is using.
Objective
To avoid the previous problem, I wish to delay the execution of all calls to asyncMoc by Xms.
Here is a graphic with what I pretend:
To achieve this I wrote the following approaches:
Using Promises
Using setInterval
Using Promises
let asyncMock = function(url) {
return new Promise(fulfil => {
setTimeout(() => {
fulfil({
url,
data: "banana"
});
}, 10000);
});
};
let delayFactory = function(args) {
let {
delayMs
} = args;
let promise = Promise.resolve();
let delayAsync = function(url) {
return promise = promise.then(() => {
return new Promise(fulfil => {
setTimeout(() => {
console.log(`made request to ${url}`);
fulfil(asyncMock(url));
}, delayMs);
});
});
};
return Object.freeze({
delayAsync
});
};
/*
* All calls to any of its functions will have a separation of X ms, and will
* all be executed in the order they were called.
*/
let delayer = delayFactory({
delayMs: 500
});
console.log('running');
delayer.delayAsync('http://www.bananas.pt')
.then(console.log)
.catch(console.error);
delayer.delayAsync('http://www.fruits.es')
.then(console.log)
.catch(console.error);
delayer.delayAsync('http://www.veggies.com')
.then(console.log)
.catch(console.error);
This factory has a function called delayAsync that will delay all calls to asyncMock by 500ms.However, it also forces the nest execution of the call to wait for the result of the previous one - which in not intended.
The objective here is to make three calls to asyncMock within 500ms each, and 10s after receive three responses with a difference of 500ms.
Using setInterval
In this approach, my objective is to have a factory which has an array of parameters. Then, every 500ms, the timer will run an executor which will take a parameter from that array and return a result with it:
/*
* This is a simulation of an async function. Be imaginative!
*/
let asyncMock = function(url) {
return new Promise(fulfil => {
setTimeout(() => {
fulfil({
url,
data: "banana"
});
}, 10000);
});
};
let delayFactory = function(args) {
let {
throttleMs
} = args;
let argsList = [];
let timer;
/*
* Every time this function is called, I add the url argument to a list of
* arguments. Then when the time comes, I take out the oldest argument and
* I run the mockGet function with it, effectively making a queue.
*/
let delayAsync = function(url) {
argsList.push(url);
return new Promise(fulfil => {
if (timer === undefined) {
console.log('created timer');
timer = setInterval(() => {
if (argsList.length === 0) {
clearInterval(timer);
timer = undefined;
} else {
let arg = argsList.shift();
console.log('making request ' + url);
fulfil(asyncMock(arg));
}
}, throttleMs);
} else {
//what if the timer is already running? I need to somehow
//connect it to this call!
}
});
};
return Object.freeze({
delayAsync
});
};
/*
* All calls to any of its functions will have a separation of X ms, and will
* all be executed in the order they were called.
*/
let delayer = delayFactory({
delayMs: 500
});
console.log('running');
delayer.delayAsync('http://www.bananas.pt')
.then(console.log)
.catch(console.error);
delayer.delayAsync('http://www.fruits.es')
.then(console.log)
.catch(console.error);
delayer.delayAsync('http://www.veggies.com')
.then(console.log)
.catch(console.error);
// a ton of other calls in random places in code
This code is even worse. It executes asyncMoch 3 times without any delay whatsoever, always with the same parameter, and then because I don't know how to complete my else branch, it does nothing.
Questions:
Which approach is better to achieve my objective and how can it be fixed?
I'm going to assume you want the promises returned by delayAsync to resolve based on the promises from asyncMock.
If so, I would use the promise-based approach and modify it like this (see comments):
// Seed our "last call at" value
let lastCall = Date.now();
let delayAsync = function(url) {
return new Promise(fulfil => {
// Delay by at least `delayMs`, but more if necessary from the last call
const now = Date.now();
const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
lastCall = now + thisDelay;
setTimeout(() => {
// Fulfill our promise using the result of `asyncMock`'s promise
fulfil(asyncMock(url));
}, thisDelay);
});
};
That ensures that each call to asyncMock is at least delayMs after the previous one (give or take a millisecond thanks to timer vagaries), and ensures the first one is delayed by at least delayMs.
Live example with some debugging info:
let lastActualCall = 0; // Debugging only
let asyncMock = function(url) {
// Start debugging
// Let's show how long since we were last called
console.log(Date.now(), "asyncMock called", lastActualCall == 0 ? "(none)" : Date.now() - lastActualCall);
lastActualCall = Date.now();
// End debugging
return new Promise(fulfil => {
setTimeout(() => {
console.log(Date.now(), "asyncMock fulfulling");
fulfil({
url,
data: "banana"
});
}, 10000);
});
};
let delayFactory = function(args) {
let {
delayMs
} = args;
// Seed our "last call at" value
let lastCall = Date.now();
let delayAsync = function(url) {
// Our new promise
return new Promise(fulfil => {
// Delay by at least `delayMs`, but more if necessary from the last call
const now = Date.now();
const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
lastCall = now + thisDelay;
console.log(Date.now(), "scheduling w/delay =", thisDelay);
setTimeout(() => {
// Fulfill our promise using the result of `asyncMock`'s promise
fulfil(asyncMock(url));
}, thisDelay);
});
};
return Object.freeze({
delayAsync
});
};
/*
* All calls to any of its functions will have a separation of X ms, and will
* all be executed in the order they were called.
*/
let delayer = delayFactory({
delayMs: 500
});
console.log('running');
delayer.delayAsync('http://www.bananas.pt')
.then(console.log)
.catch(console.error);
delayer.delayAsync('http://www.fruits.es')
.then(console.log)
.catch(console.error);
// Let's hold off for 100ms to ensure we get the spacing right
setTimeout(() => {
delayer.delayAsync('http://www.veggies.com')
.then(console.log)
.catch(console.error);
}, 100);
.as-console-wrapper {
max-height: 100% !important;
}
Okay, so here's my solution to your problem. Sorry I had to rewrite your code to better be able to understand it. I hope you can interpret it anyway and get something out of it.
Calls 500ms between eachother using Promises (JSFiddle):
function asyncFunc(url) {
return new Promise(resolve => {
setTimeout(function() {
resolve({ url: url, data: 'banana' });
}, 2000);
});
}
function delayFactory(delayMs) {
var delayMs = delayMs;
var queuedCalls = [];
var executing = false;
this.queueCall = function(url) {
var promise = new Promise(function(resolve) {
queuedCalls.push({ url: url, resolve: resolve });
executeCalls();
});
return promise;
}
var executeCalls = function() {
if(!executing) {
executing = true;
function execute(call) {
asyncFunc(call.url).then(function(result) {
call.resolve(result);
});
setTimeout(function() {
queuedCalls.splice(queuedCalls.indexOf(call), 1);
if(queuedCalls.length > 0) {
execute(queuedCalls[0]);
} else {
executing = false;
}
}, delayMs)
}
if(queuedCalls.length > 0) {
execute(queuedCalls[0]);
}
}
}
}
var factory = new delayFactory(500);
factory.queueCall('http://test1').then(console.log); //2 sec log {url: "http://test1", data: "banana"}
factory.queueCall('http://test2').then(console.log); //2.5 sec log {url: "http://test2", data: "banana"}
factory.queueCall('http://test3').then(console.log); //3 sec log {url: "http://test3", data: "banana"}
factory.queueCall('http://test4').then(console.log); //3.5 sec log {url: "http://test4", data: "banana"}
Introduction
After reading both solutions, I have to say I am very thankful to both people who took their time to help me. It is moments like this (although rare) that make me proud of having a StackOverflow account.
This said, after reading both proposals, I came with one of my own, and I will explain which one I think is best and why.
My solution
My solution is based on #Arg0n's proposal, and it is a simplification/re-implementation of his code using the factory pattern in JavaScript and defended by Douglas Crockford, using ECMA6 features:
let asyncFunc = function(url) {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve({
url: url,
data: 'banana'
});
}, 5000);
});
};
let delayFactory = function(args) {
let {
delayMs
} = args;
let queuedCalls = [];
let executing = false;
let queueCall = function(url) {
return new Promise((resolve, reject) => {
queuedCalls.push({
url,
resolve,
reject
});
if (executing === false) {
executing = true;
nextCall();
}
});
};
let execute = function(call) {
console.log(`sending request ${call.url}`);
asyncFunc(call.url)
.then(call.resolve)
.catch(call.reject);
setTimeout(nextCall, delayMs);
};
let nextCall = function() {
if (queuedCalls.length > 0)
execute(queuedCalls.shift());
else
executing = false;
};
return Object.freeze({
queueCall
});
};
let myFactory = delayFactory({
delayMs: 1000
});
myFactory.queueCall('http://test1')
.then(console.log)
.catch(console.log);
myFactory.queueCall('http://test2')
.then(console.log)
.catch(console.log);
myFactory.queueCall('http://test3')
.then(console.log)
.catch(console.log);
Why am I posting this extra solution? Because I think it is a vast improvement over Arg0n's proposal, for the following reasons:
No falsiness. Falsy values and expressions (like !executing) are a problem in JavaScript. I strongly recommend Appendix A: Awful parts of JavaScript.
Implements catch should the asyncMock fail
Use of Array.prototype.shift instead of Array.prototype.splice which is easier to read and improves performance.
No use of new keyword, no messing of the this reference
No inner functions. ESlint will thank you :P
Use of factories Douglas Crockford style
If you liked Arg0n's solution, I recommend you have a look at mine.
#Arg0n VS #T.J. Crowder ... FIGHT!
Which solution is better and why?
At first i was inclined to Arg0n's solution, because it took inspiration from one of my failed attempts and made it work. On itself, that is remarkable.
Furthermore, Timers in JavaScript have precision issues, and JavaScript also has issues when making computations with numbers (check 0.1 + 0.2 !== 0.3).
However, both solution use Timers. In fact, you need timers to achieve this behavior. Furthermore #T.J. Crowder's solution does not do arithmetic with floating points, but whole numbers, so his calculations are safe and sound.
One could point out that the Math library was a mistake in JavaScript imported from java, but honestly that is going to far and there is nothing wrong with it.
Furthermore, because T.J.'s solution does not have a data structure like Arg0n's solution has, its code is smaller as it encompasses less logic to maintain. There is no question from a technical point of view, his solution is the one to go for, in this specific case.
However, for those of you who don't master the math behind, Arg0n's avenue is a pretty solid one.
Conclusion
From a technical point of view, T.J.'s solution wins. However I can say that I enjoyed Arg0n's solution a lot, and specially my version of his post, which is the one I am likely to use.
I hope this post helps someone in the future !

This code doesn't seem to fire in order?

My problem is that the code does not seem to be running in order, as seen below.
This code is for my discord.js bot that I am creating.
var Discord = require("discord.js");
var bot = new Discord.Client();
var yt = require("C:/Users/username/Documents/Coding/Discord/youtubetest.js");
var youtubetest = new yt();
var fs = require('fs');
var youtubedl = require('youtube-dl');
var prefix = "!";
var vidid;
var commands = {
play: {
name: "!play ",
fnc: "Gets a Youtube video matching given tags.",
process: function(msg, query) {
youtubetest.respond(query, msg);
var vidid = youtubetest.vidid;
console.log(typeof(vidid) + " + " + vidid);
console.log("3");
}
}
};
bot.on('ready', () => {
console.log('I am ready!');
});
bot.on("message", msg => {
if(!msg.content.startsWith(prefix) || msg.author.bot || (msg.author.id === bot.user.id)) return;
var cmdraw = msg.content.split(" ")[0].substring(1).toLowerCase();
var query = msg.content.split("!")[1];
var cmd = commands[cmdraw];
if (cmd) {
var res = cmd.process(msg, query, bot);
if (res) {
msg.channel.sendMessage(res);
}
} else {
let msgs = [];
msgs.push(msg.content + " is not a valid command.");
msgs.push(" ");
msgs.push("Available commands:");
msgs.push(" ");
msg.channel.sendMessage(msgs);
msg.channel.sendMessage(commands.help.process(msg));
}
});
bot.on('error', e => { console.error(e); });
bot.login("mytoken");
The youtubetest.js file:
var youtube_node = require('youtube-node');
var ConfigFile = require("C:/Users/username/Documents/Coding/Discord/json_config.json");
var mybot = require("C:/Users/username/Documents/Coding/Discord/mybot.js");
function myyt () {
this.youtube = new youtube_node();
this.youtube.setKey(ConfigFile.youtube_api_key);
this.vidid = "";
}
myyt.prototype.respond = function(query, msg) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
msg.channel.sendMessage("There was an error finding requested video.");
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
}
});
console.log("2");
};
module.exports = myyt;
As the code shows, i have an object for the commands that the bot will be able to process, and I have a function to run said commands when a message is received.
Throughout the code you can see that I have put three console.logs with 1, 2 and 3 showing in which order I expect the parts of the code to run. When the code is run and a query is found the output is this:
I am ready!
string +
2
3
1
This shows that the code is running in the wrong order that I expect it to.
All help is very highly appreciated :)
*Update! Thank you all very much to understand why it isn't working. I found a solution where in the main file at vidid = youtubetest.respond(query, msg) when it does that the variable is not assigned until the function is done so it goes onto the rest of my code without the variable. To fix I simply put an if statement checking if the variable if undefined and waiting until it is defined.*
Like is mentioned before, a lot of stuff in javascript runs in async, hence the callback handlers. The reason it runs in async, is to avoid the rest of your code being "blocked" by remote calls. To avoid ending up in callback hell, most of us Javascript developers are moving more and more over to Promises. So your code could then look more like this:
myyt.prototype.respond = function(query, msg) {
return new Promise(function(resolve, reject) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
reject("There was an error finding requested video."); // passed down to the ".catch" statement below
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
resolve(2); // Resolve marks the promises as successfully completed, and passes along to the ".then" method
}
});
}).then(function(two) {
// video is now the same as myyt.vidid as above.
console.log(two);
}).catch(function(err) {
// err contains the error object from above
msg.channel.sendMessage(err);
})
};
This would naturally require a change in anything that uses this process, but creating your own prototypes seems.. odd.
This promise returns the vidid, so you'd then set vidid = youtubetest.response(query, msg);, and whenever that function gets called, you do:
vidid.then(function(id) {
// id is now the vidid.
});
Javascript runs async by design, and trying to hack your way around that leads you to dark places fast. As far as I can tell, you're also targetting nodeJS, which means that once you start running something synchronously, you'll kill off performance for other users, as everyone has to wait for that sync call to finish.
Some suggested reading:
http://callbackhell.com/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://stackoverflow.com/a/11233849/3646975
I'd also suggest looking up ES6 syntax, as it shortens your code and makes life a hellofalot easier (native promises were only introduced in ES6, which NodeJS 4 and above supports (more or less))
In javascript, please remember that any callback function you pass to some other function is called asynchronously. I.e. the calls to callback function may not happen "in order". "In order" in this case means the order they appear on the source file.
The callback function is simply called on certain event:
When there is data to be processed
on error
in your case for example when the youtube search results are ready,
'ready' event is received or 'message' is received.
etc.

Categories

Resources