How to handle async requests when message passing within chrome extensions - google-chrome-extension

Im building a chrome extension and need to get data from google cloud firestore in background.js before sending the returned data as a message to popup.js.
This is what background.js looks like:
//background.js
chrome.runtime.onMessage.addListener((msg, sender, resp) => {
if (msg.command == 'fetch') {
const listData = fetchListTitles();
resp({
type: 'result',
status: 'success',
data: listData,
request: msg,
});
return true;
}
});
} catch (e) {
console.error(e);
}
//get firestore data function
const fetchListTitles = async () => {
let listTitles = [];
const q = query(
collectionGroup(db, 'Lists'),
where('author', '==', 'placeholder')
);
const temp = await getDocs(q);
temp.docs.map((doc) => {
listTitles.push(linksToJSON(doc));
});
console.log(listTitles);
return listTitles;
};
This is what popup.js looks like
//popup.js
chrome.runtime.sendMessage({ command: 'fetch' }, (resp) => {
if (resp.data) {
console.log('popup', resp);
setListTitles(resp.data);
}
});
When I read out or console.log the data returned, I do not see any data returned from friestore. However, in background.js I can see the returned data that I console.log from the fetchListTitles function

fetchListTitles is declared with async keyword which means it always returns a Promise.
Chrome extensions can't send Promise via messaging.
You need to send the response after the Promise is fullfilled:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.command === 'fetch') {
fetchListTitles().then(listData => sendResponse({
type: 'result',
status: 'success',
data: listData,
request: msg,
}));
return true; // keeps the channel open for sendResponse
}
});
See also why Chrome extensions can't use async/await in onMessage directly.

Related

How to wait before executing the rest of code until consume/response is received from RabbitMQ (NodeJS)

This is my code sample: as you can see the logged messages are printed before a response is consumed.
const res = await channel.consume(q.queue, (msg) => {
if(msg?.properties.correlationId === '123') {
console.log("consumed");
setTimeout(() => {
connection.close();
}, 500);
return "responsed"
}
}, { noAck: true });
console.log(res);
console.log("Not working until response is received") //it will work before the response is received

Response doesn't wait for async function

I found the aproach how to put the async function into a router endpoint and tried to implement it but not suceed. E.g. this link zellwk.com
The server sends only empty string instead of a huge string that is on snippet.host.
async function downloadData(url) {
const options = {
'method': 'GET', 'url': url, 'headers': {}, formData: {}
};
let result = '';
await request(options, function (error, response) {
if (error) throw new Error(error);
console.log(response.body)
result = response.body
});
return {error: 0, text: result}
}
app.get('/token/:token', jsonParser, async function (req, res) {
console.log(req.params.token) //Don't mind this token
try {
let message = await downloadData('https://snippet.host/xgsh/raw')
res.send(message)
} catch (e) {
console.log(e);
res.sendStatus(500);
}
});
How to make it wait for the download?
EDIT
I implemented node-fetch and got this:
{"error":0,"text":{"size":0}}
What did I wrong?
async function downloadData(url) {
const result = await fetch(url)
return {error: 0, text: result}
}
app.get('/token/:token', jsonParser, async function (req, res) {
console.log(req.params.token)
try {
let message = await downloadData('https://snippet.host/xgsh/raw')
res.send(message)
} catch (e) {
console.log(e);
res.sendStatus(500);
}
});
in case of fetch call the response body need to be parsed first which you are missing.
async function downloadData(url) {
const res = await fetch(url)
if (res && res.status != 200)
throw new Error("status code other than 200 received")
let json = await res.json()
return {error: 0, text: json}
}
app.get('/token/:token', async function (req, res) {
console.log(req.params.token)
try {
let message = await downloadData('https://snippet.host/xgsh/raw')
// let json = await message.json()
res.send(message)
} catch (e) {
console.log(e);
res.sendStatus(500);
}
});
i tried calling the api but every time it is returning 404 . so json cant be called in that case. but if 200 is returned then you call json and return that accordingly.
You can only usefully await a promise.
request doesn't return a promise, it takes a callback function instead.
This is one of the reasons that request was deprecated two years ago.
Replace request with something which supports promises such as node-fetch, the native fetch that was added to Node.js recently, or axios.
In case you must use request (eventhough it's deprecated); or in case you find a similar situation with a old function that doesn't return a promise; you can turn your request into a function that returns a promise.
Either write your own wrapper, in the form of
function requestPromise(options) {
return new Promise(
(resolve,reject) => request(options,
(err,response) => if (err) reject(err) else resolve(response)));
}
or, given that request's callback is in the form of function(err,response), directly use util.promisify
async function xxxx(...) {
try {
...
let response = await util.promisify(request)(options);
...
}
Because request isn't actually an async function that returns a Promise, you can create your own async version of request.
function asyncRequest(options) {
return new Promise((resolve, reject) => {
request(options, (err, response) => {
if (err) {
reject(err)
} else {
resolve(response)
}
})
})
}
Then in your project rewrite your downloadData function to be:
async function downloadData(url) {
const options = {
method: "GET",
url: url,
headers: {},
formData: {},
}
const result = await asyncRequest(options)
return { error: 0, text: result.body }
}

Applying asynchronous functions to facebook chatbot messages in specific order

I'm doing a chatbot for facebook and I have a problem with the order of sending messages
I send in the chat:
-Hi
The bot responds in the console:
-Hello!
-How can I help you?
but in the chat responds like this:
-How can I help you?
-Hello!
I tried to apply async and await, but it didn't work.
let postWebhook = (req, res) => {
// Parse the request body from the POST
let body = req.body;
// Check the webhook event is from a Page subscription
if (body.object === 'page') {
// Iterate over each entry - there may be multiple if batched
body.entry.forEach(function (entry) {
// Gets the body of the webhook event
let webhook_event = entry.messaging[0];
// Get the sender PSID
let sender_psid = webhook_event.sender.id;
// Check if the event is a message or postback and
// pass the event to the appropriate handler function
if (webhook_event.message) {
handleMessage(sender_psid, webhook_event.message);
}
});
// Return a '200 OK' response to all events
res.status(200).send('EVENT_RECEIVED');
}};
let handleMessage = async (sender_psid, received_message) => {
let response;
if (received_message.text) {
addUrl(`url/facebook?mensage=${received_message.text}&usuario=${user}&session=${sender_psid}&origem=${chanel}`)
.then(res => res.json())
.then(async json => {
for (var i = 0; i < json.length; i++) {
console.log(json[i].text || json[i].title)
response = {json[i].text || json [i] tittle}
await callSendAPI(sender_psid, response) //function to verify user and send message
}
})
await callSendAPI(sender_psid, response);
}
};
How can I ensure the correct order?
Well you can simplify this way:
const callSendAPII = (sender_psid, response) => {
return new Promise((resolve, reject) => {
let request_body = {
"recipient": {
"id": sender_psid
},
"message": response
}
request({
uri: 'https://graph.facebook.com/v12.0/me/messages',
qs: {"access_token": MY_IG_PAGE_TOKEN},
method: 'POST',
json: request_body,
}, (error, response, body) => {
if (error) {
reject(error);
} else {
resolve(response);
}
});
})
now just apply await in the calling function:
await callSendAPII(sender_psid, response)

chrome extension - how i can await for chrome.runtime function?

My action in the background is to access the site and take information from there, the problem is that the code continues before the information is received.
Attached is a code that shows the problem:
background.js :
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
fetch(request.input, request.init).then(function(response) {
return response.text().then(function(text) {
sendResponse([{
body: text,
status: response.status,
statusText: response.statusText,
}, null]);
});
}, function(error) {
sendResponse([null, error]);
});
return true;
});
actions.js : after editing
const validateWord = async word => {
const input = "https://milog.co.il/" + word;
await new Promise(resolve => {
chrome.runtime.sendMessage({ input }, messageResponse => {
const [response, error] = messageResponse;
const parser = new DOMParser();
const html = parser.parseFromString(response.body, 'text/html');
const element = html.querySelector('.sr_e_txt');
console.log("aaa")
resolve(element?.innerText !== '');
});
});
};
validateWord("word")
.then(data => console.log(data))
.catch(reason => console.log(reason.message))
it prints aaa -> bbb -> word..
I want the "word" printed first and "aaa" wait for it to finish.
Thank you.
you can't use both a callback in a chrome method and await on the returned value because when a callback is used the method won't return a Promise.
bug in Chrome before 99: sendMessage doesn't return a Promise so you can't await it.
Fixed in Chrome 99.
So, in earlier versions of Chrome you can promisify the API:
promisifyApi(chrome.runtime, 'sendMessage');
(async () => {
const res = await chrome.runtime.sendMessage({ input });
console.log(res);
})();
function promisifyApi(thisArg, name) {
const fn = thisArg[name];
const localStack = new Error('Before calling ' + name);
thisArg[name] = (...args) => new Promise((resolve, reject) => {
fn(...args, result => {
let err = chrome.runtime.lastError;
if (err) {
err = new Error(err.message);
err.stack += '\n' + localStack.stack;
reject(err);
} else {
resolve(result);
}
});
});
}
...or use a callback:
chrome.runtime.sendMessage({ input }, res => {
// process `res` here
});

How to call an API inside a cloud function?

I'm working on an Actions on Google chatbot using Dialogflow and Cloud Functions. The runtime is Node.js 6.
Why does this function return the empty string?
function getJSON(url) {
var json = "";
var request = https.get(url, function(response) {
var body = "";
json = 'response';
response.on("data", function(chunk) {
body += chunk;
json = 'chunk';
});
response.on("end", function() {
if (response.statusCode == 200) {
try {
json = 'JSON.parse(body)';
return json;
} catch (error) {
json = 'Error1';
return json;
}
} else {
json = 'Error2';
return json;
}
});
});
return json;
}
This is the intent in which I want to access the json data:
app.intent('test', (conv) => {
conv.user.storage.test = 'no change';
const rp = require("request-promise-native");
var options = {
uri: 'https://teamtreehouse.com/joshtimonen.json',
headers: {
'User-Agent': 'Request-Promise'
},
json: true // Automatically parses the JSON string in the response
};
rp(options)
.then(function (user) {
conv.user.storage.test = user.name;
})
.catch(function (err) {
conv.user.storage.test = 'fail';
});
conv.ask(conv.user.storage.test);
});
You can try to use the request module for node.js, I tried my self reproducing your use case and worked fine. The code should be something similar to this:
const request = require('request');
request(url, {json: true}, (err, res, body) => {
if (err) { res.send(JSON.stringify({ 'fulfillmentText': "Some error"})); }
console.log(body);
});
Also, you need to add "request": "2.81.0" in the dependencies section inside your package.json file.
The function returns the empty string because https sets up a callback function, but the program flow continues to the return statement before the callback is called.
In general, when working with Dialogflow Intent Handlers, you should be returning a Promise instead of using callbacks or events. Look into using request-promise-native instead.
Two clarifying points:
You must return the Promise. Otherwise Dialogflow will assume the Handler has completed. If you return the Promise, it will wait for the Promise to finish.
Everything you want to send back must be done inside the then() block. This includes setting any response. The then() block runs after the asynchronous operation (the web API call) completes. So this will have the results of the call, and you can return these results in your call to conv.ask().
So it might look something like this:
return rp(options)
.then(function (user) {
conv.add('your name is '+user.name);
})
.catch(function (err) {
conv.add('something went wrong '+err);
});
You can use Axios as well
To install Axios : npm install axios
OR
You can add it in package.json as a dependency
"dependencies": {
"axios": "^0.27.2",
}
index.js
const axios = require('axios').default;
exports.makeRequest = async (req, res) => {
axios.get('https://jsonplaceholder.typicode.com/todos/1')// Dummy URL
.then(function (response) {
// handle success
res.status(200).json({
success:true,
result:response.data
})
})
.catch(function (error) {
// handle error
console.log(error);
})
};
OR
const Axios = require("axios");
exports.makeRequest = async (req, res) => {
const { data } = await
Axios.get('https://jsonplaceholder.typicode.com/todos/1')
res.status(200).send({data})
};

Resources