This is a program that scrapes the data out of a tshirt website and then writes the product info to a CSV file.
There are 3 scrape functions and 1 write function.
Right now, I am having an absolute nightmare trying to get my head around how to implement promises here without any 3rd party libraries or packages. Is this possible with just the native features of ES6?
Due to the async nature of the requests, I need each function and its requests to finish completely before the next one is called. This is so I can use the variables such as urlSet in the next function.
How can I do this simply without rewriting my whole code?
I should mention that each of these functions work on an individual basis, they've all been tested several times.
Does each function become an individual promise?
Code is below, thank you:
//TASK: Create a command line application that goes to an ecommerce site to get the latest prices.
//Save the scraped data in a spreadsheet (CSV format).
'use strict';
//Modules being used:
var cheerio = require('cheerio');
var json2csv = require('json2csv');
var request = require('request');
var moment = require('moment');
var fs = require('fs');
//harcoded url
var url = 'http://shirts4mike.com/';
//url for tshirt pages
var urlSet = new Set();
var remainder;
var tshirtArray = [];
// First scrape loads front page of shirts4mike and finds the first product pages/menus
function firstScrape(){
request(url, function(error, response, html) {
if(!error && response.statusCode == 200){
var $ = cheerio.load(html);
//iterate over links with 'shirt'
$('a[href*=shirt]').each(function(){
var a = $(this).attr('href');
//create new link
var scrapeLink = url + a;
//for each new link, go in and find out if there is a submit button.
//If there, add it to the set
request(scrapeLink, function(error,response, html){
if(!error && response.statusCode == 200) {
var $ = cheerio.load(html);
//if page has a submit it must be a product page
if($('[type=submit]').length !== 0){
//add page to set
urlSet.add(scrapeLink);
} else if(remainder == undefined) {
//if not a product page, add it to remainder so it another scrape can be performed.
remainder = scrapeLink;
}
}
});
});
}
});
}
//Scrape next level of menus to find remaning product pages to add to urlSet
function secondScrape() {
request(remainder, function(error, response, html) {
if(!error && response.statusCode == 200){
var $ = cheerio.load(html);
$('a[href*=shirt]').each(function(){
var a = $(this).attr('href');
//create new link
var scrapeLink = url + a;
request(scrapeLink, function(error,response, html){
if(!error && response.statusCode == 200){
var $ = cheerio.load(html);
//collect remaining product pages and add to set
if($('[type=submit]').length !== 0){
urlSet.add(scrapeLink);
}
}
});
});
}
});
}
//call lastScraper so we can grab data from the set (product pages)
function lastScraper(){
//scrape set, product pages
for(var item of urlSet){
var url = item;
request(url, function(error, response, html){
if(!error && response.statusCode == 200){
var $ = cheerio.load(html);
//grab data and store as variables
var price = $('.price').text();
var imgURL = $('.shirt-picture').find('img').attr('src');
var title = $('body').find('.shirt-details > h1').text().slice(4);
var tshirtObject = {};
//add values into tshirt object
tshirtObject.Title = title;
tshirtObject.Price = price;
tshirtObject.ImageURL = imgURL;
tshirtObject.URL = url;
tshirtObject.Date = moment().format('MMMM Do YYYY, h:mm:ss a');
//add the object into the array of tshirts
tshirtArray.push(tshirtObject);
}
});
}
}
//Convert array of tshirt objects and write to CSV file
function convertJson2Csv(){
//The scraper should generate a folder called `data` if it doesn’t exist.
var dir ='./data';
if(!fs.existsSync(dir)){
fs.mkdirSync(dir);
}
var fields = ['Title', 'Price', 'ImageURL', 'URL', 'Date'];
//convert tshirt data into CSV and pass in fields
var csv = json2csv({ data: tshirtArray, fields: fields });
//Name of file will be the date
var fileDate = moment().format('MM-DD-YY');
var fileName = dir + '/' + fileDate + '.csv';
//Write file
fs.writeFile(fileName, csv, {overwrite: true}, function(err) {
console.log('file saved');
if (err) throw err;
});
}
If you want to chain those functions with promises, then they have to return promises.
If you want to chain them with async module, then they have to take callbacks as arguments.
Right now they neither return a promise (or anything else), nor do they take callbacks (or anything else) as arguments. If the function doesn't take a callback and doesn't return anything then all you can do is call it and that's it. You will not be notified of any result.
Example
Callbacks
If you have 3 functions that take callbacks:
function fun1(cb) {
setTimeout(() => {
cb(null, "fun1");
}, 1000);
}
function fun2(cb) {
setTimeout(() => {
cb(null, "fun2");
}, 3000);
}
function fun3(cb) {
setTimeout(() => {
cb(null, "fun3");
}, 100);
}
Then you can know when they finish:
fun3((err, value) => {
console.log('fun3 finished:', value);
});
And you can easily wait for one before you start the other:
fun1((err1, val1) => {
fun2((err2, val2) => {
console.log("fun1 + fun2:", val1, val2);
});
});
Promises
If your functions return promises:
function fun1() {
return new Promise((res, rej) => {
setTimeout(() => {
res("fun1");
}, 1000);
});
}
function fun2() {
return new Promise((res, rej) => {
setTimeout(() => {
res("fun2");
}, 3000);
});
}
function fun3() {
return new Promise((res, rej) => {
setTimeout(() => {
res("fun3");
}, 100);
});
}
Then you can also know when they finish:
fun3().then(value => {
console.log('fun3 finished:', value);
});
You can also easily nest the calls:
fun1().then(val1 => {
fun2().then(val2 => {
console.log("fun1 + fun2:", val1, val2);
});
});
Or:
fun1()
.then(val1 => fun2())
.then(val2 => fun3())
.then(val3 => console.log('All 3 finished in series'));
etc.
To be able to do much more with both style, see documentation for:
http://caolan.github.io/async/
http://bluebirdjs.com/docs/getting-started.html
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
Related
I am using request NPM package and request take two parameters
request (URL, callback);
Here I somehow want to pass extra argument to my callback function how do I do it.
This is the code I am trying to write
function extractIssues(url,filepath){
request(url,issueCb);
}
function issueCb(err,response,html)
{
if(err){
console.log(err+"at line 61");
}
else{
extractIssueData(html);
}
}
function extractIssueData(html){
let selTool = cheerio.load(html);
let arr = [];
let issueLinksArr = selTool(".flex-auto.min-width-0.p-2.pr-3.pr-md-2 > a");
let result="";
for(let i = 0;i<issueLinksArr.length;i++){
let issueLink = selTool(issueLinksArr[i]).attr("href");
let content = selTool(issueLinksArr[i]).text().trim().split("/n")[0];
let obj = {
link :issueLink,
content:content
}
let str = JSON.stringify(obj);
result = result + str + " ,"+ "\n" ;
}
console.log(result);
}
I want to use filepath in extractIssueData so I need to first catch it in issueCb how do I do it
I can't find proper answer.
This issue disappears entirely if you simplify to use a single function! The following should be equivalent to, if not an improvement on, your code:
let extractIssues = async (url, filepath) => {
// Wait for the html content of a `request` call
let html = await new Promise((resolve, reject) => {
// If an Error occurs reject with the error, otherwise resolve with
// html data
request(url, (err, res, html) => err ? reject(err) : resolve(html));
});
let selTool = cheerio.load(html);
// Collect href and text content values from <a> elements
let resultsArr = selTool(".flex-auto.min-width-0.p-2.pr-3.pr-md-2 > a")
.map(issueLink => ({
link: selTool(issueLink).attr("href"),
content: selTool(issueLink).text().trim().split("/n")[0]
}));
// Print the array of link+content data
console.log(resultsArr);
};
Note that within this function you have access to filepath, so you can use it as is necessary.
A solution without async looks like:
let extractIssues = (url, filepath) => {
request(url, (err, res, html) => {
if (err) throw err;
let selTool = cheerio.load(html);
// Collect href and text content values from <a> elements
let resultsArr = selTool(".flex-auto.min-width-0.p-2.pr-3.pr-md-2 > a")
.map(issueLink => ({
link: selTool(issueLink).attr("href"),
content: selTool(issueLink).text().trim().split("/n")[0]
}));
// Print the array of link+content data
console.log(resultsArr);
});
};
Once again this creates a scenario where filepath is available to be used as necessary within the function.
Note: If you are being "given instruction" not to use async you are being done a disservice; async is a core feature and major selling point of javascript. Once you become comfortable with async you will quickly regret ever having worked without it. I recommend you take the time to understand it! :)
I can create you an easy sample to use callback functions.
It's a stupid example I know but it helps you to understand how callbacks are working.
function myFunc(callback) {
let errMessage = "This is error callback";
let message = "This is normal message";
callback(errMessage, message);
}
function myCallbackFunc(err, result) {
if(!err) {
console.log(result);
return;
}
console.log(err);
}
myFunc(myCallbackFunc);
You won't use it like this but If you want to send a error callback, you should null to response param. Like this:
function myFunc(callback) {
let errMessage = "This is error callback";
let message = "This is normal message";
let somethingBadHappened = true;
if(somethingBadHappened) {
return callback(errMessage, null);
}
callback(null, message)
}
function myCallbackFunc(err, result) {
if(!err) {
console.log(result);
return;
}
console.log(err);
}
myFunc(myCallbackFunc);
I'm new to learning Node.js, so I'm still getting used to asynchronous programming and callbacks. I'm trying to insert a record into a MS SQL Server database and return the new row's ID to my view.
The mssql query is working correctly when printed to console.log. My problem is not knowing how to properly return the data.
Here is my mssql query - in addJob.js:
var config = require('../../db/config');
async function addJob(title) {
var sql = require('mssql');
const pool = new sql.ConnectionPool(config);
var conn = pool;
let sqlResult = '';
let jobID = '';
conn.connect().then(function () {
var req = new sql.Request(conn);
req.query(`INSERT INTO Jobs (Title, ActiveJD) VALUES ('${title}', 0) ; SELECT ##IDENTITY AS JobID`).then(function (result) {
jobID = result['recordset'][0]['JobID'];
conn.close();
//This prints the correct value
console.log('jobID: ' + jobID);
}).catch(function (err) {
console.log('Unable to add job: ' + err);
conn.close();
});
}).catch(function (err) {
console.log('Unable to connect to SQL: ' + err);
});
// This prints a blank
console.log('jobID second test: ' + jobID)
return jobID;
}
module.exports = addJob;
This is my front end where a modal box is taking in a string and passing it to the above query. I want it to then receive the query's returned value and redirect to another page.
// ADD NEW JOB
$("#navButton_new").on(ace.click_event, function() {
bootbox.prompt("New Job Title", function(result) {
if (result != null) {
var job = {};
job.title = result;
$.ajax({
type: 'POST',
data: JSON.stringify(job),
contentType: 'application/json',
url: 'jds/addJob',
success: function(data) {
// this just prints that data is an object. Is that because I'm returning a promise? How would I unpack that here?
console.log('in success:' + data);
// I want to use the returned value here for a page redirect
//window.location.href = "jds/edit/?jobID=" + data;
return false;
},
error: function(err){
console.log('Unable to add job: ' + err);
}
});
} else {
}
});
});
And finally here is the express router code calling the function:
const express = require('express');
//....
const app = express();
//....
app.post('/jds/addJob', function(req, res){
let dataJSON = JSON.stringify(req.body)
let parsedData = JSON.parse(dataJSON);
const addJob = require("../models/jds/addJob");
let statusResult = addJob(parsedData.title);
statusResult.then(result => {
res.send(req.body);
});
});
I've been reading up on promises and trying to figure out what needs to change here, but I'm having no luck. Can anyone provide any tips?
You need to actually return a value from your function for things to work. Due to having nested Promises you need a couple returns here. One of the core features of promises is if you return a Promise it participates in the calling Promise chain.
So change the following lines
jobID = result['recordset'][0]['JobID'];
to
return result['recordset'][0]['JobID']
and
req.query(`INSERT INTO Jobs (Title, ActiveJD) VALUES ('${title}', 0) ; SELECT ##IDENTITY AS JobID`).then(function (result) {
to
return req.query(`INSERT INTO Jobs (Title, ActiveJD) VALUES ('${title}', 0) ; SELECT ##IDENTITY AS JobID`).then(function (result) {
and
conn.connect().then(function () {
to
return conn.connect().then(function () {
You may need to move code around that is now after the return. You would also be well served moving conn.close() into a single .finally on the end of the connect chain.
I recommend writing a test that you can use to play around with things until you get it right.
const jobId = await addJob(...)
console.log(jobId)
Alternatively rewrite the code to use await instead of .then() calls.
In Node.js I have to read files in a folder and for each file get file handler info, this is my simplest implementation using fs.readdir:
FileServer.prototype.listLocal = function (params) {
var self = this;
var options = {
limit: 100,
desc: 1
};
// override defaults
for (var attrname in params) { options[attrname] = params[attrname]; }
// media path is the media folder
var mediaDir = path.join(self._options.mediaDir, path.sep);
return new Promise((resolve, reject) => {
fs.readdir(mediaDir, (error, results) => {
if (error) {
self.logger.error("FileServer.list error:%s", error);
return reject(error);
} else { // list files
// cut to max files
results = results.slice(0, options.limit);
// filter default ext
results = results.filter(item => {
return (item.indexOf('.mp3') > -1);
});
// format meta data
results = results.map(file => {
var filePath = path.join(self._options.mediaDir, path.sep, file);
var item = {
name: file,
path: filePath
};
const fd = fs.openSync(filePath, 'r');
var fstat = fs.fstatSync(fd);
// file size in bytes
item.size = fstat.size;
item.sizehr = self.formatSizeUnits(fstat.size);
// "Birth Time" Time of file creation. Set once when the file is created.
item.birthtime = fstat.birthtime;
// "Modified Time" Time when file data last modified.
item.mtime = fstat.mtime;
// "Access Time" Time when file data last accessed.
item.atime = fstat.atime;
item.timestamp = new Date(item.mtime).getTime();
item.media_id = path.basename(filePath, '.mp3');
fs.closeSync(fd);//close file
return item;
});
if (options.desc) { // sort by most recent
results.sort(function (a, b) {
return b.timestamp - a.timestamp;
});
} else { // sort by older
results.sort(function (a, b) {
return a.timestamp - b.timestamp;
});
}
return resolve(results);
}
})
});
}
so that for each file I get an array of items
{
"name": "sample121.mp3",
"path": "/data/sample121.mp3",
"size": 5751405,
"sizehr": "5.4850 MB",
"birthtime": "2018-10-08T15:26:08.397Z",
"mtime": "2018-10-08T15:26:11.650Z",
"atime": "2018-10-10T09:01:48.534Z",
"timestamp": 1539012371650,
"media_id": "sample121"
}
That said, the problem is it's knonw that node.js fs.readdir may freeze Node I/O Loop when the folder to list has a large number of files, let's say from ten thousands to hundred thousands and more.
This is a known issue - see here for more info.
There are also plans to improve fs.readdir in a some way, like streaming - see here about this.
In the meanwhile I'm searching for like a patch to this, because my folders are pretty large.
Since the problem is the Event Loop get frozen, someone proposed a solution using process.nextTick, that I have ensembled here
FileServer.prototype.listLocalNextTick = function (params) {
var self = this;
var options = {
limit: 100,
desc: 1
};
// override defaults
for (var attrname in params) { options[attrname] = params[attrname]; }
// media path is the media folder
var mediaDir = path.join(self._options.mediaDir, path.sep);
return new Promise((resolve, reject) => {
var AsyncArrayProcessor = function (inArray, inEntryProcessingFunction) {
var elemNum = 0;
var arrLen = inArray.length;
var ArrayIterator = function () {
inEntryProcessingFunction(inArray[elemNum]);
elemNum++;
if (elemNum < arrLen) process.nextTick(ArrayIterator);
}
if (elemNum < arrLen) process.nextTick(ArrayIterator);
}
fs.readdir(mediaDir, function (error, results) {
if (error) {
self.logger.error("FileServer.list error:%s", error);
return reject(error);
}
// cut to max files
results = results.slice(0, options.limit);
// filter default ext
results = results.filter(item => {
return (item.indexOf('.mp3') > -1);
});
var ProcessDirectoryEntry = function (file) {
// This may be as complex as you may fit in a single event loop
var filePath = path.join(self._options.mediaDir, path.sep, file);
var item = {
name: file,
path: filePath
};
const fd = fs.openSync(filePath, 'r');
var fstat = fs.fstatSync(fd);
// file size in bytes
item.size = fstat.size;
item.sizehr = self.formatSizeUnits(fstat.size);
// "Birth Time" Time of file creation. Set once when the file is created.
item.birthtime = fstat.birthtime;
// "Modified Time" Time when file data last modified.
item.mtime = fstat.mtime;
// "Access Time" Time when file data last accessed.
item.atime = fstat.atime;
item.timestamp = new Date(item.mtime).getTime();
item.media_id = path.basename(filePath, '.mp3');
// map to file item
file = item;
}//ProcessDirectoryEntry
// LP: fs.readdir() callback is finished, event loop continues...
AsyncArrayProcessor(results, ProcessDirectoryEntry);
if (options.desc) { // sort by most recent
results.sort(function (a, b) {
return b.timestamp - a.timestamp;
});
} else { // sort by older
results.sort(function (a, b) {
return a.timestamp - b.timestamp;
});
}
return resolve(results);
});
});
}//listLocalNextTick
This seems to avoid the original issue, but I cannot anymore map the files lists to the items with file handler I did before, because when running the AsyncArrayProcessor on the files list, thus the ProcessDirectoryEntry on each file entry the async nature of process.nextTick causes that I cannot get back the results array modified as in the previous listLocal function where I just did an iterative array.map of the results array.
How to patch the listLocalNextTick to behave like the listLocal but keeping process.nextTick approach?
[UPDATE]
According to the proposed solution, this is the best implementation so far:
/**
* Scan files in directory
* #param {String} needle
* #param {object} options
* #returns {nodeStream}
*/
scanDirStream : function(needle,params) {
var options = {
type: 'f',
name: '*'
};
for (var attrname in params) { options[attrname] = params[attrname]; }
return new Promise((resolve, reject) => {
var opt=[needle];
for (var k in options) {
var v = options[k];
if (!Util.empty(v)) {
opt.push('-' + k);
opt.push(v);
}
};
var data='';
var listing = spawn('find',opt)
listing.stdout.on('data', _data => {
var buff=Buffer.from(_data, 'utf-8').toString();
if(buff!='') data+=buff;
})
listing.stderr.on('data', error => {
return reject(Buffer.from(error, 'utf-8').toString());
});
listing.on('close', (code) => {
var res = data.split('\n');
return resolve(res);
});
});
Example of usage:
scanDirStream(mediaRoot,{
name: '*.mp3'
})
.then(results => {
console.info("files:%d", results);
})
.catch(error => {
console.error("error %s", error);
});
This can be eventually modified to add a tick callback at every stdout.on event emitted when getting a new file in the directory listening.
I have Created a wrapper around find for it but you could use dir or ls in the same way.
const { spawn } = require('child_process');
/**
* findNodeStream
* #param {String} dir
* #returns {nodeStream}
*/
const findNodeStream = (dir,options) => spawn('find',[dir,options].flat().filter(x=>x));
/**
* Usage Example:
let listing = findNodeStream('dir',[options])
listing.stdout.on('data', d=>console.log(d.toString()))
listing.stderr.on('data', d=>console.log(d.toString()))
listing.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
*/
this allows you to stream a directory chunked and not in a whole as fs.readdir does.
Important
NodeJS > 12.11.1 will have async readdir support
Landed in cbd8d71 ( https://github.com/nodejs/node/commit/cbd8d715b2286e5726e6988921f5c870cbf74127 ) as fs{Promises}.opendir(), which returns an fs.Dir, which exposes an async iterator. tada
https://nodejs.org/api/fs.html#fs_fspromises_opendir_path_options
const fs = require('fs');
async function print(path) {
const dir = await fs.promises.opendir(path);
for await (const dirent of dir) {
console.log(dirent.name);
}
}
print('./').catch(console.error);
I tried to configure backend factory to obtain data using Amazon Products Api.
Here is the script I execute in nodejs:
var amazon = require('amazon-product-api');
var client = amazon.createClient({
awsId: 'ID',
awsSecret: 'sectret',
awsTag: 'tag',
});
// SERVER
// var Promise = require('bluebird');
var koa = require('koa');
var router = require('koa-router')();
router.get('/node/:index', function* () {
this.body = yield client.browseNodeLookup({
browseNodeId: this.params.index,
});
});
router.get('/', function* (ctx, next) {
var node = yield client.browseNodeLookup({
browseNodeId: 1063498,
});
this.body = node;
});
var app = koa();
app.use(router.routes()).use(router.allowedMethods());
app.listen(8005);
At the frontend I use Promise.map() by bluebird.js to map an array of amazon's product nodes. At the final of the function, I expect to transform links (strings) in the Array to objects (obtained by API).
Here is the function:
someFn(links) { // links is an array of node IDs
return Promise.map(links, (link) => {
var link = link;
if (typeof link === "object" || level > 1) {
return link;
} else {
return loadUrl({
url: 'http://localhost:8005/node/'+link,
action: 'json',
}).then((res) => {
if (isJSON(res)) {
return new Category(res); // transform string to object
} else {
return link;
}
});
}
});
}
Amazon allows at max 10 queries, that's why I need to run the function a few times or loop it to obtain an object for the each string in the Array.
The idea is to wait for the successful answer at the backend or repeat the query (yield client.browseNodeLookup)
Or just pass the array of node IDs and get JSON of each as the result.
I have not a lot experience with nodejs server and routing configuration, so can you help me to configure it properly?
I still did not find the solution to use backend for the task, but I've updated a loadUrl function. The function runs itself until retrieved the successful answer:
function loadUrl(options) {
var options = options;
return new Promise((resolve, reject) => {
$.post('/', options).done((result) => {
if (result != 'Internal Server Error') {
resolve(result);
} else {
(function run(x) {
var x = x || 0;
// 300 - calls limit
// for me 100 is enough
if (x <= 300) {
setTimeout(function() {
$.post('/', options).done((result) => {
if (result != 'Internal Server Error') {
resolve(result);
} else {
console.log('call', x);
run(x+1);
}
});
}, 100);
} else {
resolve(result);
}
})();
}
});
});
}
As I understood, the $.post() has time limit for the answer, and thats why I've got the problem. The data is obtained by the backend factory, but my frontend script was not ready to wait for it and was stopped.
In my node.js app, reading data from MSSQL using tedious, I'm calling the below every 1 second:
Fetch the data from the server (fetchStock function) and save it in temporary array
Send the data saved in the temporary array to the client using the Server-Sent Events (SSE) API.
It looks the 1 second is not enough to recall the fetchStock function before the previous call is completely executed, so I get execution errors from time to time.
I increased it to 5 seconds, but still get the same issue every once in a while.
How can I use Promise().then to be sure the fetchStock function is not re-called before the previouse call be completely executed?
var Request = require('tedious').Request;
var Connection = require('tedious').Connection;
var config = {
userName: 'sa',
password: 'pswd',
server: 'xx.xxx.xx.xxx',
options: {
database: 'DB',
rowCollectionOnRequestCompletion: 'true',
rowCollectionOnDone: 'true'
},
};
var sql = new Connection(config);
var addElem = (obj, elem)=> [].push.call(obj, elem);
var result = {}, tmpCol = {}, tmpRow = {};
module.exports = {
displayStock: function (es) {
var dloop = setInterval(function() {
if(result.error !== null)
if (es) es.send(JSON.stringify(result), {event: 'rmSoH', id: (new Date()).toLocaleTimeString()});
if(result.error === null)
if (es) es.send('connection is closed');
}, 1000);
},
fetchStock: function () {
request = new Request("SELECT ItemCode, WhsCode, OnHand FROM OITW where OnHand > 0 and (WhsCode ='RM' or WhsCode ='FG');", function(err, rowCount, rows) {
if (err) {
result = {'error': err};
console.log((new Date()).toLocaleTimeString()+' err : '+err);
}
if(rows)
rows.forEach(function(row){
row.forEach(function(column){
var colName = column.metadata.colName;
var value = column.value;
addElem(tmpCol, {colName: value})
});
addElem(tmpRow,{'item': tmpCol[0].colName, 'Whs': tmpCol[1].colName, 'Qty': tmpCol[2].colName});
tmpCol = {};
});
result = tmpRow;
tmpRow={}
});
sql.execSql(request);
}
}
I think what you need is a simple variable to check if there's already running request not Promise.
var latch = false;
// It will be called only if the previous call is completed
var doFetchStock = () => sql.execSql(new Request("SQL", (err, rowCount, rows) => {
// Your logic dealing with result
// Initializes the latch
latch = false;
});
module.exports = {
fetchStock: function () {
// Check if the previous request is completed or not
if (!latch) {
// Sets the latch
latch = true;
// Fetches stock
doFetchStock();
}
}
};
Actually I've used this kind of pattern a lot to allow some behavior only once.
https://github.com/cettia/cettia-javascript-client/blob/1.0.0-Beta1/cettia.js#L397-L413
https://github.com/cettia/cettia-javascript-client/blob/1.0.0-Beta1/cettia.js#L775-L797
Since javascript is mono-threaded a simple code like this should be enough on client-side
function () {
if(currentPromise != null){ // define in a closure outside
currentPromise = [..] // call to server which return a promise
currentPromise.then(function(){
currentPromise = null;
});
}
}