Accumulate ids to make a single ajax request - javascript

I have multiple places where I need to make ajax requests to fetch the items corresponding to some ids. However, I only want to make a single request by accumulating these ids and debouncing the actual method that makes the ajax request...So far I've come up with this code, but it just feels ugly/non-reusable.
Is there any simpler/recommended method to achieve similar results without sharing resolve/promise variables like I did here?
Here's a fiddle
const fakeData = [{
id: 1,
name: 'foo'
},
{
id: 2,
name: 'bar'
},
{
id: 3,
name: 'baz'
}
];
let idsToFetch = [];
let getItemsPromise, resolve, reject;
const fetchItems = _.debounce(() => {
console.log('fetching items...');
const currentResolve = resolve;
const currentReject = reject;
// simulating ajax request
setTimeout(function() {
const result = idsToFetch.map((id) => fakeData.find(item => item.id == id));
currentResolve(result);
}, 400);
getItemsPromise = resolve = reject = null;
}, 500);
function getItems(ids) {
idsToFetch = ids.filter((id) => !idsToFetch.includes(id)).concat(idsToFetch);
if (!getItemsPromise) {
getItemsPromise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
}
fetchItems();
return getItemsPromise
.then((res) => {
return res.filter((item) => ids.includes(item.id));
})
}
setTimeout(() => {
console.log('first request start');
getItems([1]).then(res => console.log('first result:', res));
}, 100);
setTimeout(() => {
console.log('second request start');
getItems([1, 2]).then(res => console.log('second result:', res));
}, 200)
setTimeout(() => {
console.log('third request start');
getItems([1, 3]).then(res => console.log('third result:', res));
}, 300)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

I found it simpler to write a debounce() function with behaviour fully under my control rather than rely on a library method.
In particular, I went out of my way to engineer a different debounce behaviour from that in the question, by which (if I understand correctly) the first, and possibly only, request of a quick sequence must wait for the debounce delay to expire.
In the code below, instead of the original getItemsPromise a debouncePromise is used to signify a debounce period, during which fetching is suppressed and request data is allowed to accumulate. From the quiescent state (whenever debouncePromise === null), the next fetch() call will fetch data ASAP (next tick). Only second and subsequent calls are debounced, until the debounce period expires and the debounce() instance returns to its quiescent state. I think that is as valid a "debounce" paradigm as the original, and arguably better. (If not, then fetch() can be lightly modified to give the original behaviour).
Apart from that, differences are minor :
messy externalized resolve and reject are avoided
in an attempt to keep debounce() generic, a resultsFilter function is passed in addition to a fetcher and delay.
Further comments in code.
function debounce(fetcher, resultsFilter, delay) {
let idsToFetch = [],
debouncePromise = null;
function reset() { // utility funtion - keeps code below clean and DRY
let idsToFetch_ = idsToFetch;
idsToFetch = [];
return idsToFetch_;
}
function fetch(ids) {
idsToFetch = idsToFetch.concat(ids.filter(id => !idsToFetch.includes(id))); // swapped around so as not to reverse the order.
if (!debouncePromise) {
// set up the debounce period, and what is to happen when it expires.
debouncePromise = new Promise(resolve => {
setTimeout(resolve, delay);
}).then(() => {
// on expiry of the debounce period ...
debouncePromise = null; // ... return to quiescent state.
return fetcher(reset()); // ... fetch (and deliver) data for all request data accumulated in the debounce period.
});
// *** First call of this debounce period - FETCH IMMEDIATELY ***
return Promise.resolve(reset()).then(fetcher); // (1) ensure fetcher is called asynchronously (as above). (2) resultsFilter is not necessary here.
} else {
return debouncePromise.then(res => resultsFilter(ids, res)); // when debouncePromise exists, return it with chained filter to give only the results for these ids.
}
}
return fetch;
}
Sample usage :
function fetchItems(ids) {
const fakeData = [
{ 'id': 1, 'name': 'foo' },
{ 'id': 2, 'name': 'bar' },
{ 'id': 3, 'name': 'baz' },
{ 'id': 4, 'name': 'zaz' }
];
if (ids.length > 0) {
return new Promise(resolve => { // simulate ajax request
setTimeout(resolve, 400);
}).then(() => {
return ids.map(id => fakeData.find(item => item.id == id));
});
} else {
return Promise.resolve([]);
}
}
function filterResults(ids, results) {
return results.filter(item => ids.includes(item.id));
}
// ******************************************************
let getItems = debounce(fetchItems, filterResults, 500);
// ******************************************************
setTimeout(() => {
console.log('first request start');
getItems([1]).then(res => console.log('first result:', res));
}, 100);
setTimeout(() => {
console.log('second request start');
getItems([1, 2]).then(res => console.log('second result:', res));
}, 200);
setTimeout(() => {
console.log('third request start');
getItems([1, 3]).then(res => console.log('third result:', res));
}, 300);
setTimeout(() => {
console.log('fourth request start');
getItems([1, 4]).then(res => console.log('fourth result:', res));
}, 2000);
Tested to the extent of this fiddle

I was able to somehow encapsulate the logic by creating a function generator that holds the previous two functions like this:
const fakeData = [{
id: 1,
name: 'foo'
},
{
id: 2,
name: 'bar'
},
{
id: 3,
name: 'baz'
}
];
function makeGetter(fetchFunc, debounceTime = 400) {
let idsToFetch = [];
let getItemsPromise, resolve, reject;
const fetchItems = _.debounce(() => {
console.log('fetching items...');
const currentResolve = resolve;
const currentReject = reject;
const currentIdsToFetch = idsToFetch;
Promise.resolve(fetchFunc(currentIdsToFetch))
.then(res => currentResolve(res))
.catch(err => currentReject(err));
getItemsPromise = resolve = reject = null;
idsToFetch = [];
}, debounceTime);
const getItems = (ids) => {
idsToFetch = ids.filter((id) => !idsToFetch.includes(id)).concat(idsToFetch);
if (!getItemsPromise) {
getItemsPromise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
}
const currentPromise = getItemsPromise;
fetchItems();
return currentPromise
.then((res) => {
return res.filter((item) => ids.includes(item.id));
})
}
return getItems;
}
const getItems = makeGetter((ids) => {
// simulating ajax request
return new Promise((resolve, reject) => {
setTimeout(function() {
const result = ids.map((id) => fakeData.find(item => item.id == id));
resolve(result);
}, 400);
})
});
setTimeout(() => {
console.log('first request start');
getItems([1]).then(res => console.log('first result:', res));
}, 100);
setTimeout(() => {
console.log('second request start');
getItems([1, 2]).then(res => console.log('second result:', res));
}, 200)
setTimeout(() => {
console.log('third request start');
getItems([1, 3]).then(res => console.log('third result:', res));
}, 300)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

Related

Async operations when instance created

I had a job interview yesterday, and I was given a coding challenge as the following:
// 3. Coding challenge.
// The goal is to make function1/function2 to work only when the constructor has finished its async operations.
// You CAN'T change the notifyUrls function. Imagine it's a 3th party library you don't have control on.
// CAN'T CHANGE THIS.
//===================
function notifyUrls(item, callback) {
asyncOperation(item).then((res) => {
callback(res);
});
}
//===================
const asyncOperation = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('Timeout execute'); resolve(); }, 2000); }); };
const URL1 = 'http://www.somerestapi/get1';
const URL2 = 'http://www.somerestapi/get2';
const URL3 = 'http://www.somerestapi/get3';
class MyClass {
constructor() {
[URL1, URL2, URL3].forEach(item => {
notifyUrls(item, () => { });
});
}
myFunction1() {
// Only start working when constructor finished notifying.
// ...
console.log('myFunction1');
}
myFunction2() {
// Only start working when constructor finished notifying.
// ...
console.log('myFunction2');
}
}
And here is what I did:
// CAN'T CHANGE THIS.
//===================
function notifyUrls(item, callback) {
asyncOperation(item).then((res) => {
callback(res);
});
}
//===================
const asyncOperation = () => { return new Promise((resolve, reject) => { setTimeout(() => { console.log('Timeout execute'); resolve(); }, 2000); }); };
const URL1 = 'http://www.somerestapi/get1';
const URL2 = 'http://www.somerestapi/get2';
const URL3 = 'http://www.somerestapi/get3';
class MyClass {
constructor() {
this.ready = Promise.all([URL1, URL2, URL3].map((url) => { this.myAsyncCall(url); }));
}
myAsyncCall(item) {
return new Promise((resolve, reject) => {
notifyUrls(item, (res) => { resolve(res); });
});
}
async myFunction1() {
if (await this.ready) {
// Only start working when constructor finished notifying.
// ...
console.log('myFunction1');
}
}
myFunction2() {
// Only start working when constructor finished notifying.
// ...
console.log('myFunction2');
}
}
(async () => {
const myClass = new MyClass();
await myClass.myFunction1();
})();
But the output is:
myFunction1
Timeout execute
Timeout execute
Timeout execute
What I want the output to be is:
Timeout execute
Timeout execute
Timeout execute
myFunction1
How can I work around it?
Thanks.
The problem is in
this.ready = Promise.all([URL1, URL2, URL3].map((url) => { this.myAsyncCall(url); }));
Your map callback doesn't return anything
Either do
this.ready = Promise.all([URL1, URL2, URL3].map((url) => { return this.myAsyncCall(url); }));
or
this.ready = Promise.all([URL1, URL2, URL3].map((url) => this.myAsyncCall(url)));
Adding some coding tips
notifyUrls(item, (res) => {
resolve(res);
});
Can simply be
notifyUrls(item, resolve);
and
const asyncOperation = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
});
};
is just
const asyncOperation = () => new Promise(resolve => setTimeout(resolve, 2000));

cancel multiple promises inside a promise on unmount?

hi i want to cancel promise on unmount since i received warning,
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
My code:
const makeCancelable = (promise: Promise<void>) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
(val) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
(error) => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
useEffect(() => {
const initialize = async () => {
const getImageFilesystemKey = (remoteUri: string) => {
const [_, fileName] = remoteUri.split('toolbox-talks/');
return `${cacheDirectory}${fileName}`;
};
const filesystemUri = getImageFilesystemKey(uri);
try {
// Use the cached image if it exists
const metadata = await getInfoAsync(filesystemUri);
if (metadata.exists) {
console.log('resolve 1');
setFileUri(filesystemUri);
} else {
const imageObject = await downloadAsync(uri, filesystemUri);
console.log('resolve 2');
setFileUri(imageObject.uri);
}
// otherwise download to cache
} catch (err) {
console.log('error 3');
setFileUri(uri);
}
};
const cancelable = makeCancelable(initialize());
cancelable.promise
.then(() => {
console.log('reslved');
})
.catch((e) => {
console.log('e ', e);
});
return () => {
cancelable.cancel();
};
}, []);
but i still get warning on fast press, help me please?
You're cancelling the promise, but you are not cancelling the axios call or any of the logic that happens after it inside initialize(). So while it is true that the console won't print resolved, setFileUri will be called regardless, which causes your problem.
A solution could look like this (untested):
const makeCancelable = (promise: Promise<void>) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
}
};
};
const initialize = async () => {
const getImageFilesystemKey = (remoteUri: string) => {
const [_, fileName] = remoteUri.split("toolbox-talks/");
return `${cacheDirectory}${fileName}`;
};
const filesystemUri = getImageFilesystemKey(uri);
try {
// Use the cached image if it exists
const metadata = await getInfoAsync(filesystemUri);
if (metadata.exists) {
console.log("resolve 1");
return filesystemUri;
} else {
const imageObject = await downloadAsync(uri, filesystemUri);
console.log("resolve 2");
return imageObject.uri;
}
// otherwise download to cache
} catch (err) {
console.error("error 3", err);
return uri;
}
};
useEffect(() => {
const cancelable = makeCancelable(initialize());
cancelable.promise.then(
fileURI => {
console.log("resolved");
setFileUri(fileURI);
},
() => {
// Your logic is such that it's only possible to get here if the promise is cancelled
console.log("cancelled");
}
);
return () => {
cancelable.cancel();
};
}, []);
This ensures that you will only call setFileUri if the promise is not cancelled (I did not check the logic of makeCancelable).

Use jest for testing timeouts calling recursive function

I want to test the following code:
const poll = (maxTries, interval, channel, stopTime) => {
let reached = 1;
const someInformation = someGetter();
const fetchData = async (resolve, reject) => {
const data = await networkClass.someApiCall();
if (data.stopTime === 1581516005) {
console.log("cond true");
someInformation.meta1 = transFormer(someInformation);
someInformation.meta2 = transFormer(someInformation);
someInformation.meta3 = {
...someInformation.meta1,
data,
};
resolve(someInformation);
} else if (reached < maxTries) {
reached += 1;
console.log("do it again");
setTimeout(fetchData, interval, resolve, reject);
} else {
reject(new Error('max retries reached'));
}
};
return new Promise(fetchData);
};
const checkForUpdates = () => {
setTimeout(() => {
poll(/* max retries */ 10, /* polling interval */ 1000, channel, stopTime)
.then((res) => {
setData(res);
console.log({ res: res.meta3.data });
})
.catch((e) => console.log({ e }));
}, 20000);
};
The test looks like that:
it(`should do stuff`, () => {
jest.spyOn(networkClass, 'someApiCall')
.mockResolvedValueOnce({ stopTime })
.mockResolvedValueOnce({ stopTime })
.mockResolvedValueOnce({ stopTime: 1581516005 });
checkForUpdates();
jest.advanceTimersByTime(40000);
expect(setDataMock).toHaveBeenCalled();
});
That console.log (console.log("do it again");) is only printed once, as if the test would not be able to call a setTimeout within a setTimeout. Do you have any ideas what might help?

How to repeatedly call asynchronous function until specified timeout?

I want to keep calling asnchronous api requests repeatedly until it exceeds specified time. Using async-retry we can only specify retrycount and interval, we wanted to specify even timeout in the parameter.
Can you just suggest a way?
// try calling apiMethod 3 times, waiting 200 ms between each retry
async.retry({times: 3, interval: 200}, apiMethod, function(err, result) {
// do something with the result
});
Here is what you want :
const scheduleTrigger = (futureDate) => {
const timeMS = new Date(futureDate) - new Date();
return new Promise((res, ref) => {
if (timeMS > 0) {
setTimeout(() => {
res();
}, timeMS);
} else {
rej();
}
})
}
//const futureDate = '2020-07-23T20:53:12';
// or
const futureDate = new Date();
futureDate.setSeconds(futureDate.getSeconds() + 5);
console.log('now');
scheduleTrigger(futureDate).then(_ => {
console.log('future date reached');
// start whatever you want
stopFlag = false;
}).catch(_ => {
// the date provided was in the past
});
const wait = (ms = 2000) => {
return new Promise(res => {
setTimeout(_ => {
res();
}, ms);
})
}
const asyncFn = _ => Promise.resolve('foo').then(x => console.log(x));
let stopFlag = true;
(async () => {
while (stopFlag) {
await asyncFn();
await wait();
}
})();
So you want to keep retrying for as long as its within a certain timeout? How about this:
// Allow retry until the timer runs out
let retry = true;
const timeout = setTimeout(() => {
// Set retry to false to disabled retrying
retry = false;
// Can also build in a cancel here
}, 10000); // 10 second timeout
const retryingCall = () => {
apiMethod()
.then(response => {
// Optionally clear the timeout
clearTimeout(timeout);
})
.catch(() => {
// If retry is still true, retry this function again
if (retry) {
retryingCall();
}
});
};
You can achieve what you want with this function:
const retryWithTimeout = ({ timeout, ...retryOptions}, apiMethod, callback) => {
let timedout = false;
const handle = setTimeout(
() => (timedout = true, callback(new Error('timeout'))),
timeout
);
return async.retry(
retryOptions,
innerCallback => timedout || apiMethod(innerCallback),
(err, result) => timedout || (clearTimeout(handle), callback(err, result))
)
};
It has the advantage of allowing you to use the functionality of async.retry, as you apparently want, and also allows the timeout to take place even when what exceeds the timeout is the apiMethod itself, not the waiting time.
Usage example:
retryWithTimeout(
{timeout: 305, times: 4, interval: 100},
(callback) => { callback('some api error'); },
(err, result) => console.log('result', err, result)
)

sinon spy doesn't register call in a generator loop?

I want to check that a piece of code is being called, so I'm using a sinon spy to assert this. However, the spy seems to be failing, despite console.logs showing that the code has been called correctly.
I'm wondering if my function being a generator is causing my spy to misreport what it's doing.
my code (i've taken out some chunks for brevity):
isBlacklisted(release, jobUUID) {
names.forEach((name) => {
this._spawnPythonProcessGenerator(
this.IS_BLACKLISTED_SCRIPT,
name
).next().value
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
}
_errorEvent(release, name, err, jobUUID) {
console.log('got here');
}
*_spawnPythonProcessGenerator(scriptSrc, name) {
const pythonProcess = this._childProcess.spawn(
'python3',
[...arguments]
);
yield new Promise((resolve, reject) => {
pythonProcess.stderr.on('data', (err) => {
reject(err.toString());
});
pythonProcess.stdout.on('data', (data) => {
resolve(data.toString());
});
});
}
and my tests:
const Blacklist = require('../../src/Blacklist2');
const childProcess = require('child_process');
const uuid = require('uuid/v4');
describe('Blacklist', () => {
let blacklist;
beforeEach(() => {
blacklist = new Blacklist(childProcess);
blacklist.IS_BLACKLISTED_SCRIPT = './test/helpers/good.py';
});
describe('isBlacklisted', () => {
it('should call the _errorEvent for every name in a release when the blacklist application is not available', async () => {
let release = {
id: 1001,
asset_controller: {
id: 54321,
},
display_name: 'Blah',
names: [
{
id: 2001,
name: 'Blah',
},
],
};
blacklist.IS_BLACKLISTED_SCRIPT = './test/helpers/'+ uuid() +'.py';
const spy = sinon.spy(blacklist, '_errorEvent');
blacklist.isBlacklisted(release, uuid());
console.log(spy);
sinon.assert.calledTwice(spy);
spy.restore();
});
});
});
my spy reports:
notCalled: true
I'll expand my comment into an actual answer, hopefully that helps.
Your problem lies with asynchrony, not with the generator. You need isBlacklisted to return a promise you can wait on. Otherwise your assertion happens before the spy is called.
Something like this:
isBlacklisted(release, jobUUID) {
let promises = names.map((name) => {
return this._spawnPythonProcessGenerator(
this.IS_BLACKLISTED_SCRIPT,
name
).next().value
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
return Promise.all(promises);
}
Then, in your test:
return blacklist.isBlacklisted(release, uuid())
.then(() => {
sinon.assert.calledTwice(spy);
});
Also... This isn't related to your problem, but your _spawnPythonProcessGenerator method doesn't need to be a generator. You're only using the first value of it by calling next like that and calling the whole thing over again for each array item.
It will work the same if you take out the *, change yield to return, and skip the .next().value when you call it. You also probably want to rename it because it's not a generator.
_spawnPythonProcess(scriptSrc, name) {
const pythonProcess = this._childProcess.spawn(
'python3',
[...arguments]
);
return new Promise((resolve, reject) => {
pythonProcess.stderr.on('data', (err) => {
reject(err.toString());
});
pythonProcess.stdout.on('data', (data) => {
resolve(data.toString());
});
});
}
When you call it:
let promises = names.map((name) => {
return this._spawnPythonProcess(
this.IS_BLACKLISTED_SCRIPT,
name
)
.then((data) => {
console.log(data);
})
.catch((err) => {
this._errorEvent(release, name, err, jobUUID);
});
}, this);
return Promise.all(promises);

Categories

Resources