Interval clearing itself in Reason - functional-programming

In Reason, what would be the most elegant way of having an interval that clears itself when some condition is satisfied? In JavaScript I could do:
var myInterval = setInterval(function () {
// do some stuff
if (fancyCondition) {
clearInterval(myInterval);
}
}, 1000);
In Reason, the best I've come up with so far is this:
let intervalIdRef = ref(None);
let clearInterval = () =>
switch (intervalIdRef^) {
| Some(intervalId) => Js.Global.clearInterval(intervalId)
| None => ()
};
let intervalId = Js.Global.setInterval(() => {
/* do some stuff */
fancyCondition ? clearInterval() : ();
}, 1000);
intervalIdRef := Some(intervalId);
Is there a way to avoid using a ref?

setInterval/clearInterval is inherently mutable, but even if it wasn't your fancyCondition would be anyway, so removing the one ref here wouldn't buy you a lot. I think even with the ref it could improved through encapsulation though, and depending slightly on your fancyCondition we should be able to get the same behavior in a purely functional way by using setTimeout instead of setInterval/clearInterval.
First, let's make your example concrete by adding a counter, printing the count, and then clearing the interval when we reach count 5, so we have something to work with:
let intervalIdRef = ref(None);
let count = ref(0);
let clearInterval = () =>
switch (intervalIdRef^) {
| Some(intervalId) => Js.Global.clearInterval(intervalId)
| None => ()
};
let intervalId = Js.Global.setInterval(() => {
if (count^ < 5) {
Js.log2("tick", count^);
count := count^ + 1;
} else {
Js.log("abort!");
clearInterval();
}
}, 200);
intervalIdRef := Some(intervalId);
The first thing I think we should do is to encapsulate the timer state/handle by wrapping it in a function and pass clearInterval to the callback instead of having it as a separate function that we might call several times without knowing if it actually does anything:
let setInterval = (timeout, action) => {
let intervalIdRef = ref(None);
let clear = () =>
switch (intervalIdRef^) {
| Some(intervalId) => Js.Global.clearInterval(intervalId)
| None => ()
};
let intervalId = Js.Global.setInterval(() => action(~clear), timeout);
intervalIdRef := Some(intervalId);
};
let count = ref(0);
setInterval(200, (~clear) => {
if (count^ < 5) {
Js.log2("tick", count^);
count := count^ + 1;
} else {
Js.log("abort!");
clear();
}
});
We've now gotten rid of the global timer handle, which I think answers your original question, but we're still stuck with count as global state. So let's get rid of that too:
let rec setTimeout = (timeout, action, state) => {
let continue = setTimeout(timeout, action);
let _:Js.Global.timeoutId =
Js.Global.setTimeout(() => action(~continue, state), timeout)
};
setTimeout(200, (~continue, count) => {
if (count < 5) {
Js.log2("tick", count);
continue(count + 1);
} else {
Js.log("abort!");
}
}, 0);
Here we've turned the problem upside down a bit. Instead of using setInterval and clearInterval and passing a clear function into our callback, we pass it a continue function that's called when we want to carry on instead of when we want to bail out. That allows us to pass state forward, and to change the state without using mutation and refs by using recursion instead. And it does so with less code. I think that'd qualify as pretty elegant, if not precisely what you asked for :)

Related

vue3 composition API - watch

I am trying to get to grips with the composition API. Struggling with watch:
const totalValuation = ref(0);
const values = ref([1, 2, 3]);
totalValuation.value = watch(finalTasksFiltered.value, () => {
return values.value.reduce((prev, curr) => {
console.log(prev + curr);
return prev + curr;
}, 0);
});
return {
finalTasksFiltered,
totalValuation,
};
The console.log works exactly like it should (1,3,6) but I cannot seem to render it to the DOM.
When I check to console it is fine but in the DOM like so {{ totalValuation }} it returns:
() => { (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.stop)(runner); if (instance) { (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.remove)(instance.effects, runner); } }
Oh, I am using Quasar - not sure if that makes a difference.
I am sure its something small.
I have imported ref and watch from vue.
I have a computed function working fine too.
watch is meant to execute a function when some value changes. It is not meant to be assigned as a ref. What you're looking to do seems like a better fit for computed
watch: (no assignment)
watch(finalTasksFiltered.value, () => {
totalValuation.value = values.value.reduce((prev, curr) => {
console.log(prev + curr);
return prev + curr;
}, 0);
});
computed: (uses assignment)
const totalValuation = computed(() => {
return values.value.reduce((prev, curr) => {
console.log(prev + curr);
return prev + curr;
}, 0);
});

crawler with ramda.js (functional programming)

I'm trying to crawl movie data from TMDB website. I finished my code with pure javascript, but I want to change the code into functional programming style by using ramda.js.
I attached my code below. I want to get rid of for-loop (if it is possible) and use R.pipe function.
(async () => {
for (let i = 0; i < 1000; i++) {
(() => {
setTimeout(async () => {
let year = startYr + Math.floor(i / 5);
await request.get(path(year, i % 5 + 1), async (err, res, data) => {
const $ = cheerio.load(data);
let list = $('.results_poster_card .poster.card .info .flex a');
_.forEach(list, (element, index) => {
listJSON.push({
MovieID: $(element).attr('id').replace('movie_', ''),
Rank: (i % 5) * 20 + index + 1,
Year: year
});
});
if(i === 1000 - 1) {
await pWriteFile(`${outputPath}/movieList.json`, JSON.stringify(listJSON, null, 2));
}
});
}, 1000 * i);
})(i);
}
})().catch(error => console.log(error));
Steps:
1- Break your code in small functions
2- Stop using async await and use promise.then(otherFunction)
3- When using promise, you could create a sleep function like these: const sleep = (time) => new Promise(resolve => setTimeout(resolve, time));
Ex.:
const process = index => sleep(1000)
.then(() => makeRequest(index))
.then(processData);
R.range(0, 1000)
.reduce(
(prev, actual) => prev.then(() => process(actual),
Promise.resolve()
) // Sequential
.then(printResult);
R.range(0, 1000)
.map(process) // Parallel
.then(printResult);
You can use the Ramda range() function to replace your loop.
https://ramdajs.com/docs/#range
R.range(0, 1000);
That will provide you with a collection of integers (your i) that you can work with (map() or whatever you need).

Puppeteer / Node - Target.createTarget - Target Closed

I'm using Node/Puppeteer in the code below, passing in a large list of URL's for traversal and scraping. It has been difficult to do it asynchronously, though I find that I am getting closer and closer to the answer. I am currently stuck on an issue related to the following error.
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 17): Error: Protocol error (Target.createTarget): Target closed.
This error occurs once upon every iteration of the while loop. Though I'm not sure what I may be doing incorrectly.
Could someone help me do the following:
1) Diagnose the source of the error.
2) Potentially find a more effective way to traverse a large list of URLs asynchronously.
async function subProc(list, batchSize) {
let subList = null;
let i = 0;
while (list.length > 0) {
let browser = await puppeteer.launch();
subList = list.splice(0, batchSize);
console.log("Master List Size :: " + list.length);
console.log("SubList Size :: " + subList.length);
for (let j = 0; j < subList.length; j++) {
promiseArray.push(new Promise((resolve, reject) => {
resolve(pageScrape(subList[j], browser));
}));
}
Promise.all(promiseArray)
.then(response => {
procArray.concat(response);
});
promiseArray = new Array();
try {
await browser.close();
} catch(ex){
console.log(ex);
}
};
}
async function pageScrape(url, browser) {
let page = await browser.newPage();
await page.goto(url, {
timeout: 0
});
await page.waitFor(1000);
return await page.evaluate(() => {
let appTitle = document.querySelector('').innerText;
let companyName = document.querySelector('').innerText;
let dateListed = document.evaluate("", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.innerText;
let category = document.evaluate("']//a//strong", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.innerText;
/* */
return {
appTitle,
companyName,
dateListed,
category
}
}).then(response => {
let urlData = {
id: subList[j],
appName: response.appTitle,
companyName: response.companyName,
dateListed: response.dateListed,
category: response.category
}
return urlData;
});
};
I figured out the solution to the problem I was having.
Every computer is limited in its processing ability, so instead of iterating through 1000 urls simultaneously you have to break it down into smaller pieces.
By using a PromiseAll, and iterating and scraping 10 urls at a time and storing these values in an array, I was able to throttle the processing required to iterate through all 1000 urls.
processBatch(subData, 10, procArray).then((processed)=>{
for(let i = 0; i < procArray.length; i++){
for(let j = 0; j < procArray[i].length; j++){
results.push(procArray[i][j]);
}
}
function processBatch(masterList, batchSize, procArray){
return Promise.all(masterList.splice(0, batchSize).map(async url =>
{
return singleScrape(url)
})).then((results) => {
if (masterList.length < batchSize) {
console.log('done');
procArray.push(results);
return procArray;
} else {
console.log('MasterList Size :: ' + masterList.length);
procArray.push(results);
return processBatch(masterList, batchSize, procArray);
}
})
}

Sails.js Async request

I would like to count how many entreprise are in some category but I'm stuck with the asynchrone concept.
Here's what I already have:
Category.getall(function(err, cat){
if(err) return res.negotiate(err);
catIds = []
for( var iCat in cat){
catIds.push(cat[iCat].id)
// and here I would like do something like
Entreprise.count({category_id: cat[iCat].id}, function(err, nbr){
categoriesOUT.push({categorie: cat, entreprise_number: nbr })
// I know that i can not do it but it's just to help to understand the logic I would like to have.
if(cat.length==iCat){
return res.json({categories: categoriesOUT})
}
})
}
})
There are a couple of ways to handle this. One would be to bring in a promise library like Q. Another would be a single database call that can count up enterprise objects grouped by category_id... however, I think that would go beyond Waterline's normal queries, you would have to use .query or .native or something.
The easiest quick fix for you is to just keep a counter of how many results you have handled. You may get tired of this approach after using it a couple of times, but it would look something like this:
Category.getall(function(err, cat){
if(err) { return res.negotiate(err); }
var catIds = [], categoriesOut = [], processedCategories = 0;
for( var iCat in cat){
catIds.push(cat[iCat].id)
Entreprise.count({category_id: cat[iCat].id}, function(err, nbr) {
if (err) {
categoriesOUT.push({categorie: cat, entreprise_number: 0});
} else {
categoriesOUT.push({categorie: cat, entreprise_number: nbr });
}
processedCategories += 1;
if (processedCategories >= cat.length) {
return res.json({categories: categoriesOUT});
}
});
}
});
Here's how I finaly get it only with MySQL request as suggered by #arbuthnott
(The category field is call domaine here)
Domaine.getall(function(err, domaines){
if(err){return res.negotiate(err)}
var domNames = {}, domContain = {}, domOut = [];
Entreprise.query('SELECT domaine_id, COUNT(*) FROM entreprise GROUP BY domaine_id', function(err, entreprises){
if(err){return res.negotiate(err)}
entreprises = JSON.parse(JSON.stringify(entreprises));
for(var ent of entreprises){
domContain[ent['domaine_id']] = ent['COUNT(*)'];
}
for(var iDom in domaines){
var countAdded = false;
for(var dc in domContain){
if(dc==domaines[iDom].id) {
domaines[iDom].entreprises_count = domContain[dc];
countAdded = true;
}
}
if(!countAdded) domaines[iDom].entreprises_count = 0;
}
res.json({domaines:domaines})
})
})

METEOR - Show loader and run methods at specific time

I wanna build a loader circle what goes from 1 to 100% and in the meantime to run some methods.
loader circle
The scenario is:
load the page and start counting.
When the counter is at 50% pause counting and run the first method and when I have the result to start counting from where I was left.
count until 90% and run the second method.
I was trying something with Meteor.setInterval on onCreated but I'm not sure if it's the right method to do this.
Can someone give me some ideas about how to approach this?
Thanks!
There are several ways you can do this depending on your specific needs and you might even want to use one of the many Reactive Timer packages that are out there.
Here is one working example that only uses the Meteor API (no packages). Note, I did not actually incorporate the loader circle animation since it wasn't specifically part of the question.
Template definition
<template name="loader">
<h1>Loading...({{loadingPercentage}})</h1>
</template>
Template logic
Template.loader.onCreated(function() {
// used to update loader circle and triggering method invocation
this.elapsedTime = new ReactiveVar(0);
// handle for the timer
var timerHandle;
// starts a timer that runs every second that can be paused or stopped later
const startTimer = () => {
timerHandle = Meteor.setInterval(() => {
this.elapsedTime.set(this.elapsedTime.get() + 1);
}, 1000);
};
// pauses/stops the timer
const pauseTimer = () => {
Meteor.clearInterval(timerHandle);
};
// let's first start our timer
startTimer();
// runs every second and triggers methods as needed
this.autorun(() => {
const elapsedTime = this.elapsedTime.get();
// call methods based upon how much time has elapsed
if (elapsedTime === 5) {
pauseTimer();
// call method and restart the timer once it completes
Meteor.call('methodOne', function(error, result) {
// do stuff with the result
startTimer();
});
} else if (elapsedTime === 9) {
pauseTimer();
// call method and restart the timer once it completes
Meteor.call('methodOne', function(error, result) {
// do stuff with the result
// DO NOT RESTART TIMER!
});
}
});
});
Template.loader.helpers({
// helper used to show elapsed time on the page
loadingPercentage: function() {
return Template.instance().elapsedTime.get();
},
});
Let me know if you have any questions.
This is what i was trying to do:
Template.onboarding.onCreated(function(){
var instance = this;
instance.progress = new ReactiveVar(0);
instance.autorun(function(){
var loader = {
maxPercent: 100,
minPercent: instance.progress.get(),
start: function(){
var self = this;
this.interval = Meteor.setInterval(function(){
self.minPercent += 1;
if(self.minPercent >= self.maxPercent) {
loader.pause();
}
if( self.minPercent == 25) {
loader.pause();
Meteor.call('runMethodOne', (err,res)=>{
if (!err) loader.resume();
});
}
if(self.minPercent == 75) {
loader.pause();
Meteor.call('runMethodTwo', (err,res) =>{
if(res) loader.resume();
});
}
}
});
}
instance.progress.set(self.minPercent)
},50);
},
pause: function(){
Meteor.clearInterval(this.interval);
delete this.interval;
},
resume: function(){
if(!this.interval) this.start();
}
};
loader.start();
}
});
});

Resources