Is there an alternative for recursive approach I use here in node.js? - node.js

I implemented a recursive function in a requestHandler I made to serialize API requests and also to make sure the endpoint isn't currently being requested. To make sure that the endpoint isn't currently being requested, I add it to a Set and verify it with conditionals.
Problem is that this recursive approach consumes quite a lot of memory when a lot of requests are made to the same endpoint. Is there any way I could make it less memory intensive as well as performant at the same time? I would love to hear any alternative approach which I could use instead of recursion. Below you can find my code.
async request(endpoint, domain, method, headers, query, body, attachments) {
const requestURL = `${(domain === "discord") ? this.discordBaseURL :
(domain === "trello") ? this.trelloBaseURL : domain}/${endpoint}`;
if (this.queueCollection.has(endpoint) === false) { // queueCollection is the Set in which I store endpoints that are currently being requested by my requestHandler.
this.queueCollection.add(endpoint);
const response = await this.conditionalsHandler(endpoint, requestURL, method, headers, query, body, attachments);
this.queueCollection.delete(endpoint);
return response;
}
else {
const response = new Promise((resolve) => {
setTimeout(() => { // https://stackoverflow.com/a/20999077
resolve(this.request(endpoint, domain, method, headers, query, body, attachments)); // This is where I make the method recursive to call itself back until the endpoint is no longer in the queueCollection Set.
}, 0);
});
return response;
}
}

Yes, you can remove the recursion by making the queueCollection a Map<string, Promise> instead of a Set<string>, and instead of recursing asynchronously and polling the queue until it's empty, chain the request to the tail of the queue if it exists like this:
async request(endpoint, domain, method, headers, query, body, attachments) {
const requestURL = `${(domain === "discord") ? this.discordBaseURL :
(domain === "trello") ? this.trelloBaseURL : domain}/${endpoint}`;
// get existing queue or create a new one
const queue = this.queueCollection.get(endpoint) || Promise.resolve();
// schedule request on the tail of the queue
const request = queue.then(
() => this.conditionalsHandler(endpoint, requestURL, method, headers, query, body, attachments)
);
// prevent errors from propagating along the queue
const tail = request.catch(() => {});
// enqueue the request
this.queueCollection.set(endpoint, tail);
try {
// propagates error handling to consumer
// waits for request to settle before executing finally block
return await request;
} finally {
// only remove promise from Map if this settled request is at the tail of the queue
if (this.queueCollection.get(endpoint) === tail) this.queueCollection.delete(endpoint);
}
}
This approach allows request to throw without breaking the chain so the consumer can handle the error and all the requests will still happen in sequence without depending on previous requests being successful, and it will always clean up the queueCollection on the last pending request regardless of whether the request throws. The await is not redundant here for that reason.

Related

How can I intercept a single XHR/fetch request, without affecting requests afterwards?

I am building a React/Typescript app, running completely client-side in browser. In componentDidMount(), I make a fetch request which I intercept successfully, to change the URL, and then make that request.
For reference, the API object is also from a third party library, loaded in via an HTML script tag, so I don't have access to the inner workings of the object. That's why I'm attempting to intercept the call instead to point the URL at a different endpoint.
async function makeRequest() {
let originalFetch = redefineFetch();
let data;
try {
data = await API.fetchData();
} catch (error) {
resetFetch(originalFetch);
return;
}
resetFetch(originalFetch);
return data;
}
const redefineFetch = () => {
const { fetch: originalFetch } = window;
let originalWindowFetch = window.fetch;
window.fetch = async (...args) => {
let [resource, config] = args;
resource = NEW_URL;
const response = await originalFetch(resource, config);
return response;
};
return originalWindowFetch;
};
const resetFetch = (
originalFetch: ((input: RequestInfo | URL, init?: RequestInit | undefined) => Promise<Response>) &
((input: RequestInfo | URL, init?: RequestInit | undefined) => Promise<Response>)
) => {
console.log("Resetting fetch");
window.fetch = originalFetch;
};
How I'm currently doing it:
I copied how it was done in this blog post. https://blog.logrocket.com/intercepting-javascript-fetch-api-requests-responses/.
As you can see, makeRequest() calls redefineFetch(), which redefines window.fetch to point to the NEW_URL instead.
redefineFetch() returns the original implementation of fetch as originalFetch.
After making the request, I call resetFetch() and pass originalFetch.
I then set window.fetch = originalFetch.
What I think is the issue
Every request including and after API.fetchData() now point to the NEW_URL.
These requests are out of my control in timing as they are made by 3rd party portions of my code.
I think I'm either not setting window.fetch back to its original value correctly, OR there's a race condition in which these mistakenly intercepted requests are being made before resetFetch() is called.
My Questions
How can I redefine fetch only for the API.fetchData() call without risking affecting any other calls made in my app?
Is there a better way to accomplish what I'm doing?

How to handle two long request simultanously in expressJS

i have an API with express one route make a few time to get all data required (search through long JSON object)
router.get(
"/:server/:maxCraftPrice/:minBenef/:from/:to",
checkJwt,
async (req, res) => {
const getAllAstuces = new Promise(async (resolve, reject) => {
const { EQUIPMENTS_DIR, RESOURCES_DIR } = paths[req.params.server];
const astuces = [];
const { from, to, maxCraftPrice, minBenef } = req.params;
const filteredEquipments = getItemByLevel(from, to);
for (const equipment in filteredEquipments) {
// parsing and push to astuces array
}
resolve(astuces);
});
const resource = await getAllAstuces;
return res.json(resource);
}
);
Now in my website when someone go to the page associated with this route, while the data is loading EVERY other request is just locked like in a queue
I tried to add Promise to handle this but no change
Is there a way to handle requests simultanously or maybe should i refactor that route to make it faster ?
If your request takes a long time to process, it will block all other requests until it is done. If you can make the request take less processing time, that's a good place to start, but you're probably going to need to take further steps to make multiple requests faster.
There are various methods for getting around this situation. This article describes a few approaches.

How can I prevent my ServiceWorker from intercepting cross-origin requests?

I am trying to clean up some code on my application https://git.sequentialread.com/forest/sequentialread-password-manager
I am using a ServiceWorker to enable the application to run offline -- however, I noticed that the ServiceWorker is intercepting cross-origin requests to backblazeb2.com. The app makes these cross origin requests as a part of its normal operation.
You can see here how I am registering the ServiceWorker:
navigator.serviceWorker.register('/serviceworker.js', {scope: "/"}).then(
(reg) => {
...
And inside the serviceworker.js code, I manually avoid caching any requests to backblazeb2.com:
...
return fetch(event.request).then(response => {
const url = new URL(event.request.url);
const isServerStorage = url.pathname.startsWith('/storage');
const isVersion = url.pathname == "/version";
const isBackblaze = url.host.includes('backblazeb2.com');
const isPut = event.request.method == "PUT";
if(!isServerStorage && !isVersion && !isBackblaze && !isPut) {
... // cache the response
However, this seems silly, I wish there was a way to limit the ServiceWorker to only intercept requests for the current origin.
I already tried inserting the origin into the scope property during registration, but this didn't work:
navigator.serviceWorker.register('/serviceworker.js', {scope: window.location.origin}).then(
(reg) => {
...
It was behaving the same way. I am assuming that perhaps this is because there are CORS headers present on the responses from backblazeb2.com, making those requests "technically" within the "scope" of the current origin ?
One idea I had, I could serve a permanent redirect from / to /static/index.html and then configure the serviceworker with a scope of /static, meaning it would only cache resources in that folder. But that seems like an ugly hack I should not have to do.
Is there a clean and "correct" way to do this??
As far as I can tell, the answer is, you can't do this. the serviceworker api won't let you.
Someone explained it to me as "the scope for the serviceworker limits where FROM the requests can be intercepted, not where TO the requests can be intercepted. So in otherwords, if I register a serviceworker at /app/, then a javascript from / or /foo/ will be able to make requests without them being intercepted.
It turns out that actually what I REALLY needed was to understand how service worker error handling works.
In the old version of my app, when the fetch() promise rejected, my code would return null.
return fetch(event.request).then(response => {
...blahblahblah...
}).catch( e => {
....
return null;
});
This was bad news bears and it was causing me to want to skip the serviceworker. what I didn't understand was; its not the serviceworkers fault per se as much as the fact that my serviceworker did not handle errors correctly. So the solution was to handle errors better.
This is what I did. I introduced a new route on the server called /error that always returns the string "serviceworker request failed".
http.HandleFunc("/error", func(response http.ResponseWriter, request *http.Request) {
response.WriteHeader(200)
fmt.Fprint(response, "serviceworker request failed")
})
And then I made sure to cache that endpoint when the service worker is installed.
self.addEventListener('install', event => {
event.waitUntil(clients.get(event.clientId).then(client => {
return caches.open(cacheVersion).then(cache => {
return cache.addAll([
'/',
'/error',
....
Finally, when the serviceworker fetch() promise rejects, I fall back to returning the cached version.
return fetch(event.request).then(response => {
...blahblahblah...
}).catch( e => {
....
return caches.match('/error');
});
I got the idea from the MDN serviceworker example project which does a similar thing and simply returns a cached image of darth vader if the fetch() promise rejects.
This allowed me to gracefully handle these errors and retry instead of silently failing. I simply had to make sure that my code does the right thing when it encounters an http response that matches the literal string "serviceworker request failed".
const requestFailedBytes = app.sjcl.codec.bytes.fromBits(app.sjcl.codec.utf8String.toBits("serviceworker request failed"));
...
var httpRequest = new XMLHttpRequest();
....
httpRequest.onloadend = () => {
...
if(app.cryptoService.uint8ArrayEquals(new Uint8Array(httpRequest.response), requestFailedBytes)) {
reject(false);
return
}
The fetch event in the service worker has a property on the request called "mode." This property allows you to check if the mode was set to "cors."
Here is an example of how to prevent cors requests.
self.addEventListener('fetch', (e) =>
{
if(e.request.method !== 'GET')
{
return false;
}
if(e.request.mode === 'navigate')
{
e.respondWith(caches.match('index.html'));
return false;
}
else if(e.request.mode === 'cors')
{
return false;
}
const response = this.fetchFile(e);
e.respondWith(response);
});

Nodejs proxy request coalescing

I'm running into an issue with my http-proxy-middleware stuff. I'm using it to proxy requests to another service which i.e. might resize images et al.
The problem is that multiple clients might call the method multiple times and thus create a stampede on the original service. I'm now looking into (what some services call request coalescing i.e. varnish) a solution that would call the service once, wait for the response and 'queue' the incoming requests with the same signature until the first is done, and return them all in a single go... This is different from 'caching' results due to the fact that I want to prevent calling the backend multiple times simultaneously and not necessarily cache the results.
I'm trying to find if something like that might be called differently or am i missing something that others have already solved someway... but i can't find anything...
As the use case seems pretty 'basic' for a reverse-proxy type setup, I would have expected alot of hits on my searches but since the problemspace is pretty generic i'm not getting anything...
Thanks!
A colleague of mine has helped my hack my own answer. It's currently used as a (express) middleware for specific GET-endpoints and basically hashes the request into a map, starts a new separate request. Concurrent incoming requests are hashed and checked and walked on the separate request callback and thus reused. This also means that if the first response is particularly slow, all coalesced requests are too
This seemed easier than to hack it into the http-proxy-middleware, but oh well, this got the job done :)
const axios = require('axios');
const responses = {};
module.exports = (req, res) => {
const queryHash = `${req.path}/${JSON.stringify(req.query)}`;
if (responses[queryHash]) {
console.log('re-using request', queryHash);
responses[queryHash].push(res);
return;
}
console.log('new request', queryHash);
const axiosConfig = {
method: req.method,
url: `[the original backend url]${req.path}`,
params: req.query,
headers: {}
};
if (req.headers.cookie) {
axiosConfig.headers.Cookie = req.headers.cookie;
}
responses[queryHash] = [res];
axios.request(axiosConfig).then((axiosRes) => {
responses[queryHash].forEach((coalescingRequest) => {
coalescingRequest.json(axiosRes.data);
});
responses[queryHash] = undefined;
}).catch((err) => {
responses[queryHash].forEach((coalescingRequest) => {
coalescingRequest.status(500).json(false);
});
responses[queryHash] = undefined;
});
};

Rx.js, handling Chrome Extension webrequest API callback functions

I'm trying to use Rx.js to handle the flow of Chrome extension webrequest API.
Each webrequest addListener() call takes a mandatory callback function as the first parameter. This sends request objects to the function. However, the callback can return a webRequest.BlockingResponse that determines the further life cycle of the request.
I'm struggling to handle the blocking response as part of the observable.
This code works well for examining all image requests, for example
onBeforeRequestHandler = function() {
var filteredURLs = ["http://*/*", "https://*/*"];
var resourceTypes = ["image"];
var filter = { urls: filteredURLs, types: resourceTypes };
var options = ["blocking"];
return Rx.Observable.create(observer => {
var listener = chrome.webRequest.onBeforeRequest.addListener(
function requestHandler(obj) {
observer.next(obj);
},
filter, options);
return unsubscribe => {
chrome.webRequest.onBeforeRequest.removeListener(listener);
};
});
};
I can then use all the Rx.js operators to manipulate the requests by doing this:
var source = onBeforeRequestHandler();
source.subscribe();
etc.
However, if during the course of working the images, I wish to cancel the request, I somehow need to return a blocking response object, like this {cancel:true} to the observable that is wrapping the chrome.webRequest.onBeforeRequest.addListener callback function.
At the moment I have no clue how to do this.
Any help much appreciated.

Resources